diff --git a/BizHawk.sln b/BizHawk.sln index 380906593ad..48143b1ab25 100644 --- a/BizHawk.sln +++ b/BizHawk.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33627.172 +# Visual Studio Version 18 +VisualStudioVersion = 18.6.11819.183 stable MinimumVisualStudioVersion = 16.0.28729.10 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BizHawk.Client.Common", "src\BizHawk.Client.Common\BizHawk.Client.Common.csproj", "{24A0AA3C-B25F-4197-B23D-476D6462DBA0}" EndProject @@ -44,73 +44,205 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|x64.ActiveCfg = Debug|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|x64.Build.0 = Debug|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|x86.ActiveCfg = Debug|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Debug|x86.Build.0 = Debug|Any CPU {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|Any CPU.Build.0 = Release|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|x64.ActiveCfg = Release|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|x64.Build.0 = Release|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|x86.ActiveCfg = Release|Any CPU + {24A0AA3C-B25F-4197-B23D-476D6462DBA0}.Release|x86.Build.0 = Release|Any CPU {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|x64.Build.0 = Debug|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Debug|x86.Build.0 = Debug|Any CPU {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|Any CPU.Build.0 = Release|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|x64.ActiveCfg = Release|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|x64.Build.0 = Release|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|x86.ActiveCfg = Release|Any CPU + {866F8D13-0678-4FF9-80A4-A3993FD4D8A3}.Release|x86.Build.0 = Release|Any CPU {DD448B37-BA3F-4544-9754-5406E8094723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD448B37-BA3F-4544-9754-5406E8094723}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Debug|x64.Build.0 = Debug|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Debug|x86.Build.0 = Debug|Any CPU {DD448B37-BA3F-4544-9754-5406E8094723}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD448B37-BA3F-4544-9754-5406E8094723}.Release|Any CPU.Build.0 = Release|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Release|x64.ActiveCfg = Release|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Release|x64.Build.0 = Release|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Release|x86.ActiveCfg = Release|Any CPU + {DD448B37-BA3F-4544-9754-5406E8094723}.Release|x86.Build.0 = Release|Any CPU {C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|x64.Build.0 = Debug|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Debug|x86.Build.0 = Debug|Any CPU {C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|Any CPU.Build.0 = Release|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|x64.ActiveCfg = Release|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|x64.Build.0 = Release|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|x86.ActiveCfg = Release|Any CPU + {C4366030-6D03-424B-AE53-F4F43BB217C3}.Release|x86.Build.0 = Release|Any CPU {F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|x64.ActiveCfg = Debug|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|x64.Build.0 = Debug|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|x86.ActiveCfg = Debug|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Debug|x86.Build.0 = Debug|Any CPU {F51946EA-827F-4D82-B841-1F2F6D060312}.Release|Any CPU.ActiveCfg = Release|Any CPU {F51946EA-827F-4D82-B841-1F2F6D060312}.Release|Any CPU.Build.0 = Release|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Release|x64.ActiveCfg = Release|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Release|x64.Build.0 = Release|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Release|x86.ActiveCfg = Release|Any CPU + {F51946EA-827F-4D82-B841-1F2F6D060312}.Release|x86.Build.0 = Release|Any CPU {E1A23168-B571-411C-B360-2229E7225E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E1A23168-B571-411C-B360-2229E7225E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Debug|x64.Build.0 = Debug|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Debug|x86.Build.0 = Debug|Any CPU {E1A23168-B571-411C-B360-2229E7225E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E1A23168-B571-411C-B360-2229E7225E0E}.Release|Any CPU.Build.0 = Release|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Release|x64.ActiveCfg = Release|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Release|x64.Build.0 = Release|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Release|x86.ActiveCfg = Release|Any CPU + {E1A23168-B571-411C-B360-2229E7225E0E}.Release|x86.Build.0 = Release|Any CPU {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|x64.Build.0 = Debug|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Debug|x86.Build.0 = Debug|Any CPU {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|Any CPU.Build.0 = Release|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|x64.ActiveCfg = Release|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|x64.Build.0 = Release|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|x86.ActiveCfg = Release|Any CPU + {197D4314-8A9F-49BA-977D-54ACEFAEB6BA}.Release|x86.Build.0 = Release|Any CPU {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|x64.Build.0 = Debug|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Debug|x86.Build.0 = Debug|Any CPU {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|Any CPU.Build.0 = Release|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|x64.ActiveCfg = Release|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|x64.Build.0 = Release|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|x86.ActiveCfg = Release|Any CPU + {9F84A0B2-861E-4EF4-B89B-5E2A3F38A465}.Release|x86.Build.0 = Release|Any CPU {17E7D20D-198C-4728-ACEC-065DE834FF02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17E7D20D-198C-4728-ACEC-065DE834FF02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Debug|x64.ActiveCfg = Debug|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Debug|x64.Build.0 = Debug|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Debug|x86.ActiveCfg = Debug|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Debug|x86.Build.0 = Debug|Any CPU {17E7D20D-198C-4728-ACEC-065DE834FF02}.Release|Any CPU.ActiveCfg = Release|Any CPU {17E7D20D-198C-4728-ACEC-065DE834FF02}.Release|Any CPU.Build.0 = Release|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Release|x64.ActiveCfg = Release|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Release|x64.Build.0 = Release|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Release|x86.ActiveCfg = Release|Any CPU + {17E7D20D-198C-4728-ACEC-065DE834FF02}.Release|x86.Build.0 = Release|Any CPU {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Debug|x64.Build.0 = Debug|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Debug|x86.Build.0 = Debug|Any CPU {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Release|Any CPU.Build.0 = Release|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Release|x64.ActiveCfg = Release|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Release|x64.Build.0 = Release|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Release|x86.ActiveCfg = Release|Any CPU + {368BC91D-48CD-492A-B6CF-B5B77F7FE7D4}.Release|x86.Build.0 = Release|Any CPU {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Debug|x64.Build.0 = Debug|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Debug|x86.Build.0 = Debug|Any CPU {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Release|Any CPU.Build.0 = Release|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Release|x64.ActiveCfg = Release|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Release|x64.Build.0 = Release|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Release|x86.ActiveCfg = Release|Any CPU + {3D050D35-B57D-4D14-BE0F-FD63552DADB0}.Release|x86.Build.0 = Release|Any CPU {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Debug|x64.Build.0 = Debug|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Debug|x86.Build.0 = Debug|Any CPU {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Release|Any CPU.Build.0 = Release|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Release|x64.ActiveCfg = Release|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Release|x64.Build.0 = Release|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Release|x86.ActiveCfg = Release|Any CPU + {E5D76DC1-84A8-47AF-BE25-E76F06D2FBBC}.Release|x86.Build.0 = Release|Any CPU {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Debug|x64.Build.0 = Debug|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Debug|x86.Build.0 = Debug|Any CPU {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Release|Any CPU.Build.0 = Release|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Release|x64.ActiveCfg = Release|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Release|x64.Build.0 = Release|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Release|x86.ActiveCfg = Release|Any CPU + {B5A2214B-3CB0-48C4-8DB1-98B38D48AC4A}.Release|x86.Build.0 = Release|Any CPU {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Debug|x64.ActiveCfg = Debug|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Debug|x64.Build.0 = Debug|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Debug|x86.ActiveCfg = Debug|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Debug|x86.Build.0 = Debug|Any CPU {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Release|Any CPU.ActiveCfg = Release|Any CPU {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Release|Any CPU.Build.0 = Release|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Release|x64.ActiveCfg = Release|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Release|x64.Build.0 = Release|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Release|x86.ActiveCfg = Release|Any CPU + {284E19E2-661D-4A7D-864A-AC2FC91E7C25}.Release|x86.Build.0 = Release|Any CPU {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Debug|x64.ActiveCfg = Debug|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Debug|x64.Build.0 = Debug|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Debug|x86.ActiveCfg = Debug|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Debug|x86.Build.0 = Debug|Any CPU {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Release|Any CPU.ActiveCfg = Release|Any CPU {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Release|Any CPU.Build.0 = Release|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Release|x64.ActiveCfg = Release|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Release|x64.Build.0 = Release|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Release|x86.ActiveCfg = Release|Any CPU + {39546396-C4D0-45D3-8C6A-D56D29B5BD72}.Release|x86.Build.0 = Release|Any CPU {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Debug|x64.Build.0 = Debug|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Debug|x86.Build.0 = Debug|Any CPU {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Release|Any CPU.Build.0 = Release|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Release|x64.ActiveCfg = Release|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Release|x64.Build.0 = Release|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Release|x86.ActiveCfg = Release|Any CPU + {D95E2B42-757A-4D19-8A76-84C6350BAD8D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/BizHawk.Client.Common/RomGame.cs b/src/BizHawk.Client.Common/RomGame.cs index bb5675db9bb..c0819aa549a 100644 --- a/src/BizHawk.Client.Common/RomGame.cs +++ b/src/BizHawk.Client.Common/RomGame.cs @@ -108,7 +108,7 @@ public RomGame(HawkFile file, string patch) { RomData = FileData; } - else if (file.Extension is ".cdt" or ".csw" or ".dsk" or ".pzx" or ".tap" or ".tzx" or ".wav") + else if (file.Extension is ".cdt" or ".csw" or ".dsk" or ".pzx" or ".tap" or ".trd" or ".scl" or ".tzx" or ".wav") { // these are not roms. unfortunately if treated as such there are certain edge-cases // where a header offset is detected. This should mitigate this issue until a cleaner solution is found diff --git a/src/BizHawk.Client.Common/RomLoader.cs b/src/BizHawk.Client.Common/RomLoader.cs index 4f3deaef772..7ccb40e8f10 100644 --- a/src/BizHawk.Client.Common/RomLoader.cs +++ b/src/BizHawk.Client.Common/RomLoader.cs @@ -465,6 +465,15 @@ private void LoadOther( _ => rom.GameInfo.System, }; + // Container-agnostic flux images (.scp/.hfe) carry no reliable platform id in their header, and + // the gamedb extension switch (in BizHawk.Emulation.Common) cannot decode flux; identify by + // content here, where the Cores flux decoders are reachable. Defers to the chooser if unsure. + if (string.IsNullOrEmpty(rom.GameInfo.System) && ext is ".scp" or ".hfe") + { + var fluxSystem = Emulation.Cores.Floppy.FluxDiskFormatIdentifier.IdentifySystem(rom.FileData); + if (!string.IsNullOrEmpty(fluxSystem)) rom.GameInfo.System = fluxSystem; + } + if (string.IsNullOrEmpty(rom.GameInfo.System)) { // Has the user picked a preference for this extension? @@ -1097,7 +1106,7 @@ private static class RomFileExtensions public static readonly IReadOnlyCollection WSWAN = new[] { "ws", "wsc", "pc2" }; - public static readonly IReadOnlyCollection ZXSpectrum = new[] { "tzx", "tap", "dsk", "pzx", "ipf" }; + public static readonly IReadOnlyCollection ZXSpectrum = new[] { "tzx", "tap", "dsk", "pzx", "ipf", "scp", "hfe", "fdi", "udi", "trd", "scl" }; public static readonly IReadOnlyCollection AutoloadFromArchive = Array.Empty() .Concat(A26) diff --git a/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs b/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs index 9a9351941f1..0f5a04da1be 100644 --- a/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs @@ -39,7 +39,12 @@ private void InitializeComponent() this.lblBorderInfo = new BizHawk.WinForms.Controls.LocLabelEx(); this.lblAutoLoadText = new BizHawk.WinForms.Controls.LocLabelEx(); this.autoLoadcheckBox1 = new System.Windows.Forms.CheckBox(); + this.flashLoadcheckBox1 = new System.Windows.Forms.CheckBox(); + this.turboMultiplierTrackBar = new System.Windows.Forms.TrackBar(); + this.turboMultiplierValueLabel = new BizHawk.WinForms.Controls.LocLabelEx(); this.textBoxCoreDetails = new System.Windows.Forms.TextBox(); + this.gigascreenBlendCheckBox = new System.Windows.Forms.CheckBox(); + ((System.ComponentModel.ISupportInitialize)(this.turboMultiplierTrackBar)).BeginInit(); this.SuspendLayout(); // // OkBtn @@ -143,7 +148,37 @@ private void InitializeComponent() this.autoLoadcheckBox1.TabIndex = 26; this.autoLoadcheckBox1.Text = "Auto-Load Tape"; this.autoLoadcheckBox1.UseVisualStyleBackColor = true; - // + // + // flashLoadcheckBox1 + // + this.flashLoadcheckBox1.AutoSize = true; + this.flashLoadcheckBox1.Location = new System.Drawing.Point(170, 302); + this.flashLoadcheckBox1.Name = "flashLoadcheckBox1"; + this.flashLoadcheckBox1.Size = new System.Drawing.Size(180, 17); + this.flashLoadcheckBox1.TabIndex = 27; + this.flashLoadcheckBox1.Text = "Turbo Loading"; + this.flashLoadcheckBox1.UseVisualStyleBackColor = true; + // + // turboMultiplierTrackBar + // + // internal range 1..10 maps to 5x..50x (i.e. multiplier = Value * 5), so the slider only ever lands + // on multiples of 5 + this.turboMultiplierTrackBar.Location = new System.Drawing.Point(268, 298); + this.turboMultiplierTrackBar.Minimum = 1; + this.turboMultiplierTrackBar.Maximum = 10; + this.turboMultiplierTrackBar.Name = "turboMultiplierTrackBar"; + this.turboMultiplierTrackBar.Size = new System.Drawing.Size(120, 30); + this.turboMultiplierTrackBar.TabIndex = 29; + this.turboMultiplierTrackBar.TickStyle = System.Windows.Forms.TickStyle.None; + this.turboMultiplierTrackBar.Value = 4; + this.turboMultiplierTrackBar.Scroll += new System.EventHandler(this.TurboMultiplierTrackBar_Scroll); + // + // turboMultiplierValueLabel + // + this.turboMultiplierValueLabel.Location = new System.Drawing.Point(392, 302); + this.turboMultiplierValueLabel.Name = "turboMultiplierValueLabel"; + this.turboMultiplierValueLabel.Text = "20x"; + // // textBoxCoreDetails // this.textBoxCoreDetails.AcceptsReturn = true; @@ -156,17 +191,31 @@ private void InitializeComponent() this.textBoxCoreDetails.ScrollBars = System.Windows.Forms.ScrollBars.Both; this.textBoxCoreDetails.Size = new System.Drawing.Size(424, 206); this.textBoxCoreDetails.TabIndex = 28; - // + // + // gigascreenBlendCheckBox + // + this.gigascreenBlendCheckBox.AutoSize = true; + this.gigascreenBlendCheckBox.Location = new System.Drawing.Point(15, 414); + this.gigascreenBlendCheckBox.Name = "gigascreenBlendCheckBox"; + this.gigascreenBlendCheckBox.Size = new System.Drawing.Size(300, 17); + this.gigascreenBlendCheckBox.TabIndex = 30; + this.gigascreenBlendCheckBox.Text = "Gigascreen frame blending (smooths per-frame flicker)"; + this.gigascreenBlendCheckBox.UseVisualStyleBackColor = true; + // // ZXSpectrumCoreEmulationSettings - // + // this.AcceptButton = this.OkBtn; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.CancelBtn; this.ClientSize = new System.Drawing.Size(448, 469); this.Controls.Add(this.textBoxCoreDetails); + this.Controls.Add(this.gigascreenBlendCheckBox); this.Controls.Add(this.lblAutoLoadText); this.Controls.Add(this.autoLoadcheckBox1); + this.Controls.Add(this.flashLoadcheckBox1); + this.Controls.Add(this.turboMultiplierTrackBar); + this.Controls.Add(this.turboMultiplierValueLabel); this.Controls.Add(this.lblBorderInfo); this.Controls.Add(this.label2); this.Controls.Add(this.borderTypecomboBox1); @@ -181,6 +230,7 @@ private void InitializeComponent() this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "Core Emulation Settings"; this.Load += new System.EventHandler(this.IntvControllerSettings_Load); + ((System.ComponentModel.ISupportInitialize)(this.turboMultiplierTrackBar)).EndInit(); this.ResumeLayout(false); this.PerformLayout(); @@ -199,6 +249,10 @@ private void InitializeComponent() private BizHawk.WinForms.Controls.LocLabelEx lblBorderInfo; private BizHawk.WinForms.Controls.LocLabelEx lblAutoLoadText; private System.Windows.Forms.CheckBox autoLoadcheckBox1; + private System.Windows.Forms.CheckBox flashLoadcheckBox1; + private System.Windows.Forms.TrackBar turboMultiplierTrackBar; + private BizHawk.WinForms.Controls.LocLabelEx turboMultiplierValueLabel; private System.Windows.Forms.TextBox textBoxCoreDetails; + private System.Windows.Forms.CheckBox gigascreenBlendCheckBox; } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs b/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs index 3adfb93f8c0..ef9b413fc63 100644 --- a/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs +++ b/src/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs @@ -11,10 +11,16 @@ public partial class ZxSpectrumCoreEmulationSettings : Form private readonly ZXSpectrum.ZXSpectrumSyncSettings _syncSettings; + // Gigascreen frame blending is a display-only (non-sync) Setting, but it is shown here alongside the + // other picture options (border type). It is saved separately via PutCoreSettings so toggling it does + // not reboot the core or affect movie sync. + private readonly ZXSpectrum.ZXSpectrumSettings _settings; + public ZxSpectrumCoreEmulationSettings(ISettingsAdapter settable) { _settable = settable; _syncSettings = (ZXSpectrum.ZXSpectrumSyncSettings) _settable.GetSyncSettings(); + _settings = (ZXSpectrum.ZXSpectrumSettings) _settable.GetSettings(); InitializeComponent(); Icon = Properties.Resources.GameControllerIcon; } @@ -44,15 +50,52 @@ private void IntvControllerSettings_Load(object sender, EventArgs e) // autoload tape autoLoadcheckBox1.Checked = _syncSettings.AutoLoadTape; + + // gigascreen frame blending (non-sync display setting) + gigascreenBlendCheckBox.Checked = _settings.GigascreenFrameBlend; + + // turbo tape loading - only takes effect when Deterministic Emulation is off, so the turbo controls + // are disabled (greyed out) whenever deterministic emulation is enabled + flashLoadcheckBox1.Checked = _syncSettings.TapeLoadSpeed == ZXSpectrum.TapeLoadSpeed.Instant; + + // turbo multiplier slider: internal range 1..10 maps to 5x..50x (multiplier = Value * 5) + int storedMult = _syncSettings.TapeLoadTurboMultiplier; + if (storedMult < 5) storedMult = 5; + else if (storedMult > 50) storedMult = 50; + turboMultiplierTrackBar.Value = storedMult / 5; + UpdateTurboMultiplierLabel(); + + UpdateTurboControlsEnabled(); + determEmucheckBox1.CheckedChanged += (_, _) => UpdateTurboControlsEnabled(); + flashLoadcheckBox1.CheckedChanged += (_, _) => UpdateTurboControlsEnabled(); + } + + private void TurboMultiplierTrackBar_Scroll(object sender, EventArgs e) => UpdateTurboMultiplierLabel(); + + private void UpdateTurboMultiplierLabel() => turboMultiplierValueLabel.Text = $"{turboMultiplierTrackBar.Value * 5}x"; + + private void UpdateTurboControlsEnabled() + { + // turbo loading only applies on a non-deterministic core + flashLoadcheckBox1.Enabled = !determEmucheckBox1.Checked; + // the multiplier only matters when turbo loading is both available and ticked + bool turboActive = flashLoadcheckBox1.Enabled && flashLoadcheckBox1.Checked; + turboMultiplierTrackBar.Enabled = turboActive; + turboMultiplierValueLabel.Enabled = turboActive; } private void OkBtn_Click(object sender, EventArgs e) { + var tapeLoadSpeed = flashLoadcheckBox1.Checked ? ZXSpectrum.TapeLoadSpeed.Instant : ZXSpectrum.TapeLoadSpeed.Accurate; + int turboMultiplier = turboMultiplierTrackBar.Value * 5; + bool changed = _syncSettings.MachineType.ToString() != MachineSelectionComboBox.SelectedItem.ToString() || _syncSettings.BorderType.ToString() != borderTypecomboBox1.SelectedItem.ToString() || _syncSettings.DeterministicEmulation != determEmucheckBox1.Checked - || _syncSettings.AutoLoadTape != autoLoadcheckBox1.Checked; + || _syncSettings.AutoLoadTape != autoLoadcheckBox1.Checked + || _syncSettings.TapeLoadSpeed != tapeLoadSpeed + || _syncSettings.TapeLoadTurboMultiplier != turboMultiplier; if (changed) { @@ -60,9 +103,19 @@ private void OkBtn_Click(object sender, EventArgs e) _syncSettings.BorderType = (ZXSpectrum.BorderType)Enum.Parse(typeof(ZXSpectrum.BorderType), borderTypecomboBox1.SelectedItem.ToString()); _syncSettings.DeterministicEmulation = determEmucheckBox1.Checked; _syncSettings.AutoLoadTape = autoLoadcheckBox1.Checked; + _syncSettings.TapeLoadSpeed = tapeLoadSpeed; + _syncSettings.TapeLoadTurboMultiplier = turboMultiplier; _settable.PutCoreSyncSettings(_syncSettings); } + + // non-sync display setting: applied live (no reboot), so handled independently of 'changed' above + if (gigascreenBlendCheckBox.Checked != _settings.GigascreenFrameBlend) + { + _settings.GigascreenFrameBlend = gigascreenBlendCheckBox.Checked; + _settable.PutCoreSettings(_settings); + } + DialogResult = DialogResult.OK; Close(); } diff --git a/src/BizHawk.Emulation.Common/Database/Database.cs b/src/BizHawk.Emulation.Common/Database/Database.cs index 3afe0e8b4bd..9aef5244956 100644 --- a/src/BizHawk.Emulation.Common/Database/Database.cs +++ b/src/BizHawk.Emulation.Common/Database/Database.cs @@ -402,6 +402,9 @@ public static GameInfo GetGameInfo(byte[] romData, string fileName) case ".PZX": case ".CSW": case ".WAV": + case ".UDI": // Ultra Disk Image - ZX Spectrum only + case ".TRD": // TR-DOS disk - ZX Spectrum (Beta 128 / Pentagon) only + case ".SCL": // TR-DOS packed disk - ZX Spectrum only game.System = VSystemID.Raw.ZXSpectrum; break; diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Disassemblable.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Disassemblable.cs new file mode 100644 index 00000000000..7dad153676d --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Disassemblable.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components.Z80A; // reuse the shared Z80ADisassembler tables + +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + // IDisassemblable is implemented by delegating to the shared Z80ADisassembler + // (the mnemonic tables live in the original Z80A core and are not perf-critical, + // so they are reused rather than forked). + public sealed partial class Z80AOpt : IDisassemblable + { + public string Cpu + { + get => "Z80"; + set { } + } + + public string PCRegisterName => "PC"; + + public IEnumerable AvailableCpus { get; } = [ "Z80" ]; + + public string Disassemble(MemoryDomain m, uint addr, out int length) + => Z80ADisassembler.Disassemble((ushort)addr, a => m.PeekByte(a), out length); + } +} diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Execute.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Execute.cs new file mode 100644 index 00000000000..32afae62654 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Execute.cs @@ -0,0 +1,1454 @@ +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + public partial class Z80AOpt + { + public long TotalExecutedCycles; + + private int EI_pending; + // ZXHawk needs to be able to read this for zx-state snapshot export + public int EIPending => EI_pending; + + public const ushort CBpre = 0; + public const ushort EXTDpre = 1; + public const ushort IXpre = 2; + public const ushort IYpre = 3; + public const ushort IXCBpre = 4; + public const ushort IYCBpre = 5; + public const ushort IXYprefetch = 6; + public ushort PRE_SRC; + + // variables for executing instructions + public int instr_pntr = 0; + public int bus_pntr = 0; + public int mem_pntr = 0; + public int irq_pntr = 0; + public ushort[] cur_instr = new ushort[38]; // fixed size - do not change at runtime + public ushort[] BUSRQ = new ushort[19]; // fixed size - do not change at runtime + public ushort[] MEMRQ = new ushort[19]; // fixed size - do not change at runtime + public int IRQS; + public byte opcode; + public bool NO_prefix, CB_prefix, IX_prefix, EXTD_prefix, IY_prefix, IXCB_prefix, IYCB_prefix; + public bool halted; + public bool I_skip; + + public void FetchInstruction() + { + if (NO_prefix) + { + switch (opcode) + { + case 0x00: NOP_(); break; // NOP + case 0x01: LD_IND_16(C, B, PCl, PCh); break; // LD BC, nn + case 0x02: LD_8_IND(C, B, A); break; // LD (BC), A + case 0x03: INC_16(C, B); break; // INC BC + case 0x04: INT_OP(INC8, B); break; // INC B + case 0x05: INT_OP(DEC8, B); break; // DEC B + case 0x06: LD_IND_8_INC(B, PCl, PCh); break; // LD B, n + case 0x07: INT_OP(RLC, Aim); break; // RLCA + case 0x08: EXCH_(); break; // EXCH AF, AF' + case 0x09: ADD_16(L, H, C, B); break; // ADD HL, BC + case 0x0A: REG_OP_IND(TR, A, C, B); break; // LD A, (BC) + case 0x0B: DEC_16(C, B); break; // DEC BC + case 0x0C: INT_OP(INC8, C); break; // INC C + case 0x0D: INT_OP(DEC8, C); break; // DEC C + case 0x0E: LD_IND_8_INC(C, PCl, PCh); break; // LD C, n + case 0x0F: INT_OP(RRC, Aim); break; // RRCA + case 0x10: DJNZ_(); break; // DJNZ B + case 0x11: LD_IND_16(E, D, PCl, PCh); break; // LD DE, nn + case 0x12: LD_8_IND(E, D, A); break; // LD (DE), A + case 0x13: INC_16(E, D); break; // INC DE + case 0x14: INT_OP(INC8, D); break; // INC D + case 0x15: INT_OP(DEC8, D); break; // DEC D + case 0x16: LD_IND_8_INC(D, PCl, PCh); break; // LD D, n + case 0x17: INT_OP(RL, Aim); break; // RLA + case 0x18: JR_COND(true); break; // JR, r8 + case 0x19: ADD_16(L, H, E, D); break; // ADD HL, DE + case 0x1A: REG_OP_IND(TR, A, E, D); break; // LD A, (DE) + case 0x1B: DEC_16(E, D); break; // DEC DE + case 0x1C: INT_OP(INC8, E); break; // INC E + case 0x1D: INT_OP(DEC8, E); break; // DEC E + case 0x1E: LD_IND_8_INC(E, PCl, PCh); break; // LD E, n + case 0x1F: INT_OP(RR, Aim); break; // RRA + case 0x20: JR_COND(!FlagZ); break; // JR NZ, r8 + case 0x21: LD_IND_16(L, H, PCl, PCh); break; // LD HL, nn + case 0x22: LD_16_IND_nn(L, H); break; // LD (nn), HL + case 0x23: INC_16(L, H); break; // INC HL + case 0x24: INT_OP(INC8, H); break; // INC H + case 0x25: INT_OP(DEC8, H); break; // DEC H + case 0x26: LD_IND_8_INC(H, PCl, PCh); break; // LD H, n + case 0x27: INT_OP(DA, A); break; // DAA + case 0x28: JR_COND(FlagZ); break; // JR Z, r8 + case 0x29: ADD_16(L, H, L, H); break; // ADD HL, HL + case 0x2A: LD_IND_16_nn(L, H); break; // LD HL, (nn) + case 0x2B: DEC_16(L, H); break; // DEC HL + case 0x2C: INT_OP(INC8, L); break; // INC L + case 0x2D: INT_OP(DEC8, L); break; // DEC L + case 0x2E: LD_IND_8_INC(L, PCl, PCh); break; // LD L, n + case 0x2F: INT_OP(CPL, A); break; // CPL + case 0x30: JR_COND(!FlagC); break; // JR NC, r8 + case 0x31: LD_IND_16(SPl, SPh, PCl, PCh); break; // LD SP, nn + case 0x32: LD_8_IND_nn(A); break; // LD (nn), A + case 0x33: INC_16(SPl, SPh); break; // INC SP + case 0x34: INC_8_IND(L, H); break; // INC (HL) + case 0x35: DEC_8_IND(L, H); break; // DEC (HL) + case 0x36: LD_8_IND_IND(L, H, PCl, PCh); break; // LD (HL), n + case 0x37: INT_OP(SCF, A); break; // SCF + case 0x38: JR_COND(FlagC); break; // JR C, r8 + case 0x39: ADD_16(L, H, SPl, SPh); break; // ADD HL, SP + case 0x3A: LD_IND_8_nn(A); break; // LD A, (nn) + case 0x3B: DEC_16(SPl, SPh); break; // DEC SP + case 0x3C: INT_OP(INC8, A); break; // INC A + case 0x3D: INT_OP(DEC8, A); break; // DEC A + case 0x3E: LD_IND_8_INC(A, PCl, PCh); break; // LD A, n + case 0x3F: INT_OP(CCF, A); break; // CCF + case 0x40: REG_OP(TR, B, B); break; // LD B, B + case 0x41: REG_OP(TR, B, C); break; // LD B, C + case 0x42: REG_OP(TR, B, D); break; // LD B, D + case 0x43: REG_OP(TR, B, E); break; // LD B, E + case 0x44: REG_OP(TR, B, H); break; // LD B, H + case 0x45: REG_OP(TR, B, L); break; // LD B, L + case 0x46: REG_OP_IND_HL(TR, B); break; // LD B, (HL) + case 0x47: REG_OP(TR, B, A); break; // LD B, A + case 0x48: REG_OP(TR, C, B); break; // LD C, B + case 0x49: REG_OP(TR, C, C); break; // LD C, C + case 0x4A: REG_OP(TR, C, D); break; // LD C, D + case 0x4B: REG_OP(TR, C, E); break; // LD C, E + case 0x4C: REG_OP(TR, C, H); break; // LD C, H + case 0x4D: REG_OP(TR, C, L); break; // LD C, L + case 0x4E: REG_OP_IND_HL(TR, C); break; // LD C, (HL) + case 0x4F: REG_OP(TR, C, A); break; // LD C, A + case 0x50: REG_OP(TR, D, B); break; // LD D, B + case 0x51: REG_OP(TR, D, C); break; // LD D, C + case 0x52: REG_OP(TR, D, D); break; // LD D, D + case 0x53: REG_OP(TR, D, E); break; // LD D, E + case 0x54: REG_OP(TR, D, H); break; // LD D, H + case 0x55: REG_OP(TR, D, L); break; // LD D, L + case 0x56: REG_OP_IND_HL(TR, D); break; // LD D, (HL) + case 0x57: REG_OP(TR, D, A); break; // LD D, A + case 0x58: REG_OP(TR, E, B); break; // LD E, B + case 0x59: REG_OP(TR, E, C); break; // LD E, C + case 0x5A: REG_OP(TR, E, D); break; // LD E, D + case 0x5B: REG_OP(TR, E, E); break; // LD E, E + case 0x5C: REG_OP(TR, E, H); break; // LD E, H + case 0x5D: REG_OP(TR, E, L); break; // LD E, L + case 0x5E: REG_OP_IND_HL(TR, E); break; // LD E, (HL) + case 0x5F: REG_OP(TR, E, A); break; // LD E, A + case 0x60: REG_OP(TR, H, B); break; // LD H, B + case 0x61: REG_OP(TR, H, C); break; // LD H, C + case 0x62: REG_OP(TR, H, D); break; // LD H, D + case 0x63: REG_OP(TR, H, E); break; // LD H, E + case 0x64: REG_OP(TR, H, H); break; // LD H, H + case 0x65: REG_OP(TR, H, L); break; // LD H, L + case 0x66: REG_OP_IND_HL(TR, H); break; // LD H, (HL) + case 0x67: REG_OP(TR, H, A); break; // LD H, A + case 0x68: REG_OP(TR, L, B); break; // LD L, B + case 0x69: REG_OP(TR, L, C); break; // LD L, C + case 0x6A: REG_OP(TR, L, D); break; // LD L, D + case 0x6B: REG_OP(TR, L, E); break; // LD L, E + case 0x6C: REG_OP(TR, L, H); break; // LD L, H + case 0x6D: REG_OP(TR, L, L); break; // LD L, L + case 0x6E: REG_OP_IND_HL(TR, L); break; // LD L, (HL) + case 0x6F: REG_OP(TR, L, A); break; // LD L, A + case 0x70: LD_8_IND_HL(B); break; // LD (HL), B + case 0x71: LD_8_IND_HL(C); break; // LD (HL), C + case 0x72: LD_8_IND_HL(D); break; // LD (HL), D + case 0x73: LD_8_IND_HL(E); break; // LD (HL), E + case 0x74: LD_8_IND_HL(H); break; // LD (HL), H + case 0x75: LD_8_IND_HL(L); break; // LD (HL), L + case 0x76: HALT_(); break; // HALT + case 0x77: LD_8_IND_HL( A); break; // LD (HL), A + case 0x78: REG_OP(TR, A, B); break; // LD A, B + case 0x79: REG_OP(TR, A, C); break; // LD A, C + case 0x7A: REG_OP(TR, A, D); break; // LD A, D + case 0x7B: REG_OP(TR, A, E); break; // LD A, E + case 0x7C: REG_OP(TR, A, H); break; // LD A, H + case 0x7D: REG_OP(TR, A, L); break; // LD A, L + case 0x7E: REG_OP_IND_HL(TR, A); break; // LD A, (HL) + case 0x7F: REG_OP(TR, A, A); break; // LD A, A + case 0x80: REG_OP(ADD8, A, B); break; // ADD A, B + case 0x81: REG_OP(ADD8, A, C); break; // ADD A, C + case 0x82: REG_OP(ADD8, A, D); break; // ADD A, D + case 0x83: REG_OP(ADD8, A, E); break; // ADD A, E + case 0x84: REG_OP(ADD8, A, H); break; // ADD A, H + case 0x85: REG_OP(ADD8, A, L); break; // ADD A, L + case 0x86: REG_OP_IND(ADD8, A, L, H); break; // ADD A, (HL) + case 0x87: REG_OP(ADD8, A, A); break; // ADD A, A + case 0x88: REG_OP(ADC8, A, B); break; // ADC A, B + case 0x89: REG_OP(ADC8, A, C); break; // ADC A, C + case 0x8A: REG_OP(ADC8, A, D); break; // ADC A, D + case 0x8B: REG_OP(ADC8, A, E); break; // ADC A, E + case 0x8C: REG_OP(ADC8, A, H); break; // ADC A, H + case 0x8D: REG_OP(ADC8, A, L); break; // ADC A, L + case 0x8E: REG_OP_IND(ADC8, A, L, H); break; // ADC A, (HL) + case 0x8F: REG_OP(ADC8, A, A); break; // ADC A, A + case 0x90: REG_OP(SUB8, A, B); break; // SUB A, B + case 0x91: REG_OP(SUB8, A, C); break; // SUB A, C + case 0x92: REG_OP(SUB8, A, D); break; // SUB A, D + case 0x93: REG_OP(SUB8, A, E); break; // SUB A, E + case 0x94: REG_OP(SUB8, A, H); break; // SUB A, H + case 0x95: REG_OP(SUB8, A, L); break; // SUB A, L + case 0x96: REG_OP_IND(SUB8, A, L, H); break; // SUB A, (HL) + case 0x97: REG_OP(SUB8, A, A); break; // SUB A, A + case 0x98: REG_OP(SBC8, A, B); break; // SBC A, B + case 0x99: REG_OP(SBC8, A, C); break; // SBC A, C + case 0x9A: REG_OP(SBC8, A, D); break; // SBC A, D + case 0x9B: REG_OP(SBC8, A, E); break; // SBC A, E + case 0x9C: REG_OP(SBC8, A, H); break; // SBC A, H + case 0x9D: REG_OP(SBC8, A, L); break; // SBC A, L + case 0x9E: REG_OP_IND(SBC8, A, L, H); break; // SBC A, (HL) + case 0x9F: REG_OP(SBC8, A, A); break; // SBC A, A + case 0xA0: REG_OP(AND8, A, B); break; // AND A, B + case 0xA1: REG_OP(AND8, A, C); break; // AND A, C + case 0xA2: REG_OP(AND8, A, D); break; // AND A, D + case 0xA3: REG_OP(AND8, A, E); break; // AND A, E + case 0xA4: REG_OP(AND8, A, H); break; // AND A, H + case 0xA5: REG_OP(AND8, A, L); break; // AND A, L + case 0xA6: REG_OP_IND(AND8, A, L, H); break; // AND A, (HL) + case 0xA7: REG_OP(AND8, A, A); break; // AND A, A + case 0xA8: REG_OP(XOR8, A, B); break; // XOR A, B + case 0xA9: REG_OP(XOR8, A, C); break; // XOR A, C + case 0xAA: REG_OP(XOR8, A, D); break; // XOR A, D + case 0xAB: REG_OP(XOR8, A, E); break; // XOR A, E + case 0xAC: REG_OP(XOR8, A, H); break; // XOR A, H + case 0xAD: REG_OP(XOR8, A, L); break; // XOR A, L + case 0xAE: REG_OP_IND(XOR8, A, L, H); break; // XOR A, (HL) + case 0xAF: REG_OP(XOR8, A, A); break; // XOR A, A + case 0xB0: REG_OP(OR8, A, B); break; // OR A, B + case 0xB1: REG_OP(OR8, A, C); break; // OR A, C + case 0xB2: REG_OP(OR8, A, D); break; // OR A, D + case 0xB3: REG_OP(OR8, A, E); break; // OR A, E + case 0xB4: REG_OP(OR8, A, H); break; // OR A, H + case 0xB5: REG_OP(OR8, A, L); break; // OR A, L + case 0xB6: REG_OP_IND(OR8, A, L, H); break; // OR A, (HL) + case 0xB7: REG_OP(OR8, A, A); break; // OR A, A + case 0xB8: REG_OP(CP8, A, B); break; // CP A, B + case 0xB9: REG_OP(CP8, A, C); break; // CP A, C + case 0xBA: REG_OP(CP8, A, D); break; // CP A, D + case 0xBB: REG_OP(CP8, A, E); break; // CP A, E + case 0xBC: REG_OP(CP8, A, H); break; // CP A, H + case 0xBD: REG_OP(CP8, A, L); break; // CP A, L + case 0xBE: REG_OP_IND(CP8, A, L, H); break; // CP A, (HL) + case 0xBF: REG_OP(CP8, A, A); break; // CP A, A + case 0xC0: RET_COND(!FlagZ); break; // Ret NZ + case 0xC1: POP_(C, B); break; // POP BC + case 0xC2: JP_COND(!FlagZ); break; // JP NZ + case 0xC3: JP_COND(true); break; // JP + case 0xC4: CALL_COND(!FlagZ); break; // CALL NZ + case 0xC5: PUSH_(C, B); break; // PUSH BC + case 0xC6: REG_OP_IND_INC(ADD8, A, PCl, PCh); break; // ADD A, n + case 0xC7: RST_(0); break; // RST 0 + case 0xC8: RET_COND(FlagZ); break; // RET Z + case 0xC9: RET_(); break; // RET + case 0xCA: JP_COND(FlagZ); break; // JP Z + case 0xCB: PREFIX_(CBpre); break; // PREFIX CB + case 0xCC: CALL_COND(FlagZ); break; // CALL Z + case 0xCD: CALL_COND(true); break; // CALL + case 0xCE: REG_OP_IND_INC(ADC8, A, PCl, PCh); break; // ADC A, n + case 0xCF: RST_(0x08); break; // RST 0x08 + case 0xD0: RET_COND(!FlagC); break; // Ret NC + case 0xD1: POP_(E, D); break; // POP DE + case 0xD2: JP_COND(!FlagC); break; // JP NC + case 0xD3: OUT_(); break; // OUT A + case 0xD4: CALL_COND(!FlagC); break; // CALL NC + case 0xD5: PUSH_(E, D); break; // PUSH DE + case 0xD6: REG_OP_IND_INC(SUB8, A, PCl, PCh); break; // SUB A, n + case 0xD7: RST_(0x10); break; // RST 0x10 + case 0xD8: RET_COND(FlagC); break; // RET C + case 0xD9: EXX_(); break; // EXX + case 0xDA: JP_COND(FlagC); break; // JP C + case 0xDB: IN_(); break; // IN A + case 0xDC: CALL_COND(FlagC); break; // CALL C + case 0xDD: PREFIX_(IXpre); break; // PREFIX IX + case 0xDE: REG_OP_IND_INC(SBC8, A, PCl, PCh); break; // SBC A, n + case 0xDF: RST_(0x18); break; // RST 0x18 + case 0xE0: RET_COND(!FlagP); break; // RET Po + case 0xE1: POP_(L, H); break; // POP HL + case 0xE2: JP_COND(!FlagP); break; // JP Po + case 0xE3: EXCH_16_IND_(SPl, SPh, L, H); break; // ex (SP), HL + case 0xE4: CALL_COND(!FlagP); break; // CALL Po + case 0xE5: PUSH_(L, H); break; // PUSH HL + case 0xE6: REG_OP_IND_INC(AND8, A, PCl, PCh); break; // AND A, n + case 0xE7: RST_(0x20); break; // RST 0x20 + case 0xE8: RET_COND(FlagP); break; // RET Pe + case 0xE9: JP_16(L, H); break; // JP (HL) + case 0xEA: JP_COND(FlagP); break; // JP Pe + case 0xEB: EXCH_16_(E,D, L, H); break; // ex DE, HL + case 0xEC: CALL_COND(FlagP); break; // CALL Pe + case 0xED: PREFIX_(EXTDpre); break; // PREFIX EXTD + case 0xEE: REG_OP_IND_INC(XOR8, A, PCl, PCh); break; // XOR A, n + case 0xEF: RST_(0x28); break; // RST 0x28 + case 0xF0: RET_COND(!FlagS); break; // RET p + case 0xF1: POP_(F, A); break; // POP AF + case 0xF2: JP_COND(!FlagS); break; // JP p + case 0xF3: DI_(); break; // DI + case 0xF4: CALL_COND(!FlagS); break; // CALL p + case 0xF5: PUSH_(F, A); break; // PUSH AF + case 0xF6: REG_OP_IND_INC(OR8, A, PCl, PCh); break; // OR A, n + case 0xF7: RST_(0x30); break; // RST 0x30 + case 0xF8: RET_COND(FlagS); break; // RET M + case 0xF9: LD_SP_16(L, H); break; // LD SP, HL + case 0xFA: JP_COND(FlagS); break; // JP M + case 0xFB: EI_(); break; // EI + case 0xFC: CALL_COND(FlagS); break; // CALL M + case 0xFD: PREFIX_(IYpre); break; // PREFIX IY + case 0xFE: REG_OP_IND_INC(CP8, A, PCl, PCh); break; // CP A, n + case 0xFF: RST_(0x38); break; // RST 0x38 + } + } + else if (CB_prefix) + { + CB_prefix = false; + NO_prefix = true; + switch (opcode) + { + case 0x00: INT_OP(RLC, B); break; // RLC B + case 0x01: INT_OP(RLC, C); break; // RLC C + case 0x02: INT_OP(RLC, D); break; // RLC D + case 0x03: INT_OP(RLC, E); break; // RLC E + case 0x04: INT_OP(RLC, H); break; // RLC H + case 0x05: INT_OP(RLC, L); break; // RLC L + case 0x06: INT_OP_IND(RLC, L, H); break; // RLC (HL) + case 0x07: INT_OP(RLC, A); break; // RLC A + case 0x08: INT_OP(RRC, B); break; // RRC B + case 0x09: INT_OP(RRC, C); break; // RRC C + case 0x0A: INT_OP(RRC, D); break; // RRC D + case 0x0B: INT_OP(RRC, E); break; // RRC E + case 0x0C: INT_OP(RRC, H); break; // RRC H + case 0x0D: INT_OP(RRC, L); break; // RRC L + case 0x0E: INT_OP_IND(RRC, L, H); break; // RRC (HL) + case 0x0F: INT_OP(RRC, A); break; // RRC A + case 0x10: INT_OP(RL, B); break; // RL B + case 0x11: INT_OP(RL, C); break; // RL C + case 0x12: INT_OP(RL, D); break; // RL D + case 0x13: INT_OP(RL, E); break; // RL E + case 0x14: INT_OP(RL, H); break; // RL H + case 0x15: INT_OP(RL, L); break; // RL L + case 0x16: INT_OP_IND(RL, L, H); break; // RL (HL) + case 0x17: INT_OP(RL, A); break; // RL A + case 0x18: INT_OP(RR, B); break; // RR B + case 0x19: INT_OP(RR, C); break; // RR C + case 0x1A: INT_OP(RR, D); break; // RR D + case 0x1B: INT_OP(RR, E); break; // RR E + case 0x1C: INT_OP(RR, H); break; // RR H + case 0x1D: INT_OP(RR, L); break; // RR L + case 0x1E: INT_OP_IND(RR, L, H); break; // RR (HL) + case 0x1F: INT_OP(RR, A); break; // RR A + case 0x20: INT_OP(SLA, B); break; // SLA B + case 0x21: INT_OP(SLA, C); break; // SLA C + case 0x22: INT_OP(SLA, D); break; // SLA D + case 0x23: INT_OP(SLA, E); break; // SLA E + case 0x24: INT_OP(SLA, H); break; // SLA H + case 0x25: INT_OP(SLA, L); break; // SLA L + case 0x26: INT_OP_IND(SLA, L, H); break; // SLA (HL) + case 0x27: INT_OP(SLA, A); break; // SLA A + case 0x28: INT_OP(SRA, B); break; // SRA B + case 0x29: INT_OP(SRA, C); break; // SRA C + case 0x2A: INT_OP(SRA, D); break; // SRA D + case 0x2B: INT_OP(SRA, E); break; // SRA E + case 0x2C: INT_OP(SRA, H); break; // SRA H + case 0x2D: INT_OP(SRA, L); break; // SRA L + case 0x2E: INT_OP_IND(SRA, L, H); break; // SRA (HL) + case 0x2F: INT_OP(SRA, A); break; // SRA A + case 0x30: INT_OP(SLL, B); break; // SLL B + case 0x31: INT_OP(SLL, C); break; // SLL C + case 0x32: INT_OP(SLL, D); break; // SLL D + case 0x33: INT_OP(SLL, E); break; // SLL E + case 0x34: INT_OP(SLL, H); break; // SLL H + case 0x35: INT_OP(SLL, L); break; // SLL L + case 0x36: INT_OP_IND(SLL, L, H); break; // SLL (HL) + case 0x37: INT_OP(SLL, A); break; // SLL A + case 0x38: INT_OP(SRL, B); break; // SRL B + case 0x39: INT_OP(SRL, C); break; // SRL C + case 0x3A: INT_OP(SRL, D); break; // SRL D + case 0x3B: INT_OP(SRL, E); break; // SRL E + case 0x3C: INT_OP(SRL, H); break; // SRL H + case 0x3D: INT_OP(SRL, L); break; // SRL L + case 0x3E: INT_OP_IND(SRL, L, H); break; // SRL (HL) + case 0x3F: INT_OP(SRL, A); break; // SRL A + case 0x40: BIT_OP(BIT, 0, B); break; // BIT 0, B + case 0x41: BIT_OP(BIT, 0, C); break; // BIT 0, C + case 0x42: BIT_OP(BIT, 0, D); break; // BIT 0, D + case 0x43: BIT_OP(BIT, 0, E); break; // BIT 0, E + case 0x44: BIT_OP(BIT, 0, H); break; // BIT 0, H + case 0x45: BIT_OP(BIT, 0, L); break; // BIT 0, L + case 0x46: BIT_TE_IND(BIT, 0, L, H); break; // BIT 0, (HL) + case 0x47: BIT_OP(BIT, 0, A); break; // BIT 0, A + case 0x48: BIT_OP(BIT, 1, B); break; // BIT 1, B + case 0x49: BIT_OP(BIT, 1, C); break; // BIT 1, C + case 0x4A: BIT_OP(BIT, 1, D); break; // BIT 1, D + case 0x4B: BIT_OP(BIT, 1, E); break; // BIT 1, E + case 0x4C: BIT_OP(BIT, 1, H); break; // BIT 1, H + case 0x4D: BIT_OP(BIT, 1, L); break; // BIT 1, L + case 0x4E: BIT_TE_IND(BIT, 1, L, H); break; // BIT 1, (HL) + case 0x4F: BIT_OP(BIT, 1, A); break; // BIT 1, A + case 0x50: BIT_OP(BIT, 2, B); break; // BIT 2, B + case 0x51: BIT_OP(BIT, 2, C); break; // BIT 2, C + case 0x52: BIT_OP(BIT, 2, D); break; // BIT 2, D + case 0x53: BIT_OP(BIT, 2, E); break; // BIT 2, E + case 0x54: BIT_OP(BIT, 2, H); break; // BIT 2, H + case 0x55: BIT_OP(BIT, 2, L); break; // BIT 2, L + case 0x56: BIT_TE_IND(BIT, 2, L, H); break; // BIT 2, (HL) + case 0x57: BIT_OP(BIT, 2, A); break; // BIT 2, A + case 0x58: BIT_OP(BIT, 3, B); break; // BIT 3, B + case 0x59: BIT_OP(BIT, 3, C); break; // BIT 3, C + case 0x5A: BIT_OP(BIT, 3, D); break; // BIT 3, D + case 0x5B: BIT_OP(BIT, 3, E); break; // BIT 3, E + case 0x5C: BIT_OP(BIT, 3, H); break; // BIT 3, H + case 0x5D: BIT_OP(BIT, 3, L); break; // BIT 3, L + case 0x5E: BIT_TE_IND(BIT, 3, L, H); break; // BIT 3, (HL) + case 0x5F: BIT_OP(BIT, 3, A); break; // BIT 3, A + case 0x60: BIT_OP(BIT, 4, B); break; // BIT 4, B + case 0x61: BIT_OP(BIT, 4, C); break; // BIT 4, C + case 0x62: BIT_OP(BIT, 4, D); break; // BIT 4, D + case 0x63: BIT_OP(BIT, 4, E); break; // BIT 4, E + case 0x64: BIT_OP(BIT, 4, H); break; // BIT 4, H + case 0x65: BIT_OP(BIT, 4, L); break; // BIT 4, L + case 0x66: BIT_TE_IND(BIT, 4, L, H); break; // BIT 4, (HL) + case 0x67: BIT_OP(BIT, 4, A); break; // BIT 4, A + case 0x68: BIT_OP(BIT, 5, B); break; // BIT 5, B + case 0x69: BIT_OP(BIT, 5, C); break; // BIT 5, C + case 0x6A: BIT_OP(BIT, 5, D); break; // BIT 5, D + case 0x6B: BIT_OP(BIT, 5, E); break; // BIT 5, E + case 0x6C: BIT_OP(BIT, 5, H); break; // BIT 5, H + case 0x6D: BIT_OP(BIT, 5, L); break; // BIT 5, L + case 0x6E: BIT_TE_IND(BIT, 5, L, H); break; // BIT 5, (HL) + case 0x6F: BIT_OP(BIT, 5, A); break; // BIT 5, A + case 0x70: BIT_OP(BIT, 6, B); break; // BIT 6, B + case 0x71: BIT_OP(BIT, 6, C); break; // BIT 6, C + case 0x72: BIT_OP(BIT, 6, D); break; // BIT 6, D + case 0x73: BIT_OP(BIT, 6, E); break; // BIT 6, E + case 0x74: BIT_OP(BIT, 6, H); break; // BIT 6, H + case 0x75: BIT_OP(BIT, 6, L); break; // BIT 6, L + case 0x76: BIT_TE_IND(BIT, 6, L, H); break; // BIT 6, (HL) + case 0x77: BIT_OP(BIT, 6, A); break; // BIT 6, A + case 0x78: BIT_OP(BIT, 7, B); break; // BIT 7, B + case 0x79: BIT_OP(BIT, 7, C); break; // BIT 7, C + case 0x7A: BIT_OP(BIT, 7, D); break; // BIT 7, D + case 0x7B: BIT_OP(BIT, 7, E); break; // BIT 7, E + case 0x7C: BIT_OP(BIT, 7, H); break; // BIT 7, H + case 0x7D: BIT_OP(BIT, 7, L); break; // BIT 7, L + case 0x7E: BIT_TE_IND(BIT, 7, L, H); break; // BIT 7, (HL) + case 0x7F: BIT_OP(BIT, 7, A); break; // BIT 7, A + case 0x80: BIT_OP(RES, 0, B); break; // RES 0, B + case 0x81: BIT_OP(RES, 0, C); break; // RES 0, C + case 0x82: BIT_OP(RES, 0, D); break; // RES 0, D + case 0x83: BIT_OP(RES, 0, E); break; // RES 0, E + case 0x84: BIT_OP(RES, 0, H); break; // RES 0, H + case 0x85: BIT_OP(RES, 0, L); break; // RES 0, L + case 0x86: BIT_OP_IND(RES, 0, L, H); break; // RES 0, (HL) + case 0x87: BIT_OP(RES, 0, A); break; // RES 0, A + case 0x88: BIT_OP(RES, 1, B); break; // RES 1, B + case 0x89: BIT_OP(RES, 1, C); break; // RES 1, C + case 0x8A: BIT_OP(RES, 1, D); break; // RES 1, D + case 0x8B: BIT_OP(RES, 1, E); break; // RES 1, E + case 0x8C: BIT_OP(RES, 1, H); break; // RES 1, H + case 0x8D: BIT_OP(RES, 1, L); break; // RES 1, L + case 0x8E: BIT_OP_IND(RES, 1, L, H); break; // RES 1, (HL) + case 0x8F: BIT_OP(RES, 1, A); break; // RES 1, A + case 0x90: BIT_OP(RES, 2, B); break; // RES 2, B + case 0x91: BIT_OP(RES, 2, C); break; // RES 2, C + case 0x92: BIT_OP(RES, 2, D); break; // RES 2, D + case 0x93: BIT_OP(RES, 2, E); break; // RES 2, E + case 0x94: BIT_OP(RES, 2, H); break; // RES 2, H + case 0x95: BIT_OP(RES, 2, L); break; // RES 2, L + case 0x96: BIT_OP_IND(RES, 2, L, H); break; // RES 2, (HL) + case 0x97: BIT_OP(RES, 2, A); break; // RES 2, A + case 0x98: BIT_OP(RES, 3, B); break; // RES 3, B + case 0x99: BIT_OP(RES, 3, C); break; // RES 3, C + case 0x9A: BIT_OP(RES, 3, D); break; // RES 3, D + case 0x9B: BIT_OP(RES, 3, E); break; // RES 3, E + case 0x9C: BIT_OP(RES, 3, H); break; // RES 3, H + case 0x9D: BIT_OP(RES, 3, L); break; // RES 3, L + case 0x9E: BIT_OP_IND(RES, 3, L, H); break; // RES 3, (HL) + case 0x9F: BIT_OP(RES, 3, A); break; // RES 3, A + case 0xA0: BIT_OP(RES, 4, B); break; // RES 4, B + case 0xA1: BIT_OP(RES, 4, C); break; // RES 4, C + case 0xA2: BIT_OP(RES, 4, D); break; // RES 4, D + case 0xA3: BIT_OP(RES, 4, E); break; // RES 4, E + case 0xA4: BIT_OP(RES, 4, H); break; // RES 4, H + case 0xA5: BIT_OP(RES, 4, L); break; // RES 4, L + case 0xA6: BIT_OP_IND(RES, 4, L, H); break; // RES 4, (HL) + case 0xA7: BIT_OP(RES, 4, A); break; // RES 4, A + case 0xA8: BIT_OP(RES, 5, B); break; // RES 5, B + case 0xA9: BIT_OP(RES, 5, C); break; // RES 5, C + case 0xAA: BIT_OP(RES, 5, D); break; // RES 5, D + case 0xAB: BIT_OP(RES, 5, E); break; // RES 5, E + case 0xAC: BIT_OP(RES, 5, H); break; // RES 5, H + case 0xAD: BIT_OP(RES, 5, L); break; // RES 5, L + case 0xAE: BIT_OP_IND(RES, 5, L, H); break; // RES 5, (HL) + case 0xAF: BIT_OP(RES, 5, A); break; // RES 5, A + case 0xB0: BIT_OP(RES, 6, B); break; // RES 6, B + case 0xB1: BIT_OP(RES, 6, C); break; // RES 6, C + case 0xB2: BIT_OP(RES, 6, D); break; // RES 6, D + case 0xB3: BIT_OP(RES, 6, E); break; // RES 6, E + case 0xB4: BIT_OP(RES, 6, H); break; // RES 6, H + case 0xB5: BIT_OP(RES, 6, L); break; // RES 6, L + case 0xB6: BIT_OP_IND(RES, 6, L, H); break; // RES 6, (HL) + case 0xB7: BIT_OP(RES, 6, A); break; // RES 6, A + case 0xB8: BIT_OP(RES, 7, B); break; // RES 7, B + case 0xB9: BIT_OP(RES, 7, C); break; // RES 7, C + case 0xBA: BIT_OP(RES, 7, D); break; // RES 7, D + case 0xBB: BIT_OP(RES, 7, E); break; // RES 7, E + case 0xBC: BIT_OP(RES, 7, H); break; // RES 7, H + case 0xBD: BIT_OP(RES, 7, L); break; // RES 7, L + case 0xBE: BIT_OP_IND(RES, 7, L, H); break; // RES 7, (HL) + case 0xBF: BIT_OP(RES, 7, A); break; // RES 7, A + case 0xC0: BIT_OP(SET, 0, B); break; // SET 0, B + case 0xC1: BIT_OP(SET, 0, C); break; // SET 0, C + case 0xC2: BIT_OP(SET, 0, D); break; // SET 0, D + case 0xC3: BIT_OP(SET, 0, E); break; // SET 0, E + case 0xC4: BIT_OP(SET, 0, H); break; // SET 0, H + case 0xC5: BIT_OP(SET, 0, L); break; // SET 0, L + case 0xC6: BIT_OP_IND(SET, 0, L, H); break; // SET 0, (HL) + case 0xC7: BIT_OP(SET, 0, A); break; // SET 0, A + case 0xC8: BIT_OP(SET, 1, B); break; // SET 1, B + case 0xC9: BIT_OP(SET, 1, C); break; // SET 1, C + case 0xCA: BIT_OP(SET, 1, D); break; // SET 1, D + case 0xCB: BIT_OP(SET, 1, E); break; // SET 1, E + case 0xCC: BIT_OP(SET, 1, H); break; // SET 1, H + case 0xCD: BIT_OP(SET, 1, L); break; // SET 1, L + case 0xCE: BIT_OP_IND(SET, 1, L, H); break; // SET 1, (HL) + case 0xCF: BIT_OP(SET, 1, A); break; // SET 1, A + case 0xD0: BIT_OP(SET, 2, B); break; // SET 2, B + case 0xD1: BIT_OP(SET, 2, C); break; // SET 2, C + case 0xD2: BIT_OP(SET, 2, D); break; // SET 2, D + case 0xD3: BIT_OP(SET, 2, E); break; // SET 2, E + case 0xD4: BIT_OP(SET, 2, H); break; // SET 2, H + case 0xD5: BIT_OP(SET, 2, L); break; // SET 2, L + case 0xD6: BIT_OP_IND(SET, 2, L, H); break; // SET 2, (HL) + case 0xD7: BIT_OP(SET, 2, A); break; // SET 2, A + case 0xD8: BIT_OP(SET, 3, B); break; // SET 3, B + case 0xD9: BIT_OP(SET, 3, C); break; // SET 3, C + case 0xDA: BIT_OP(SET, 3, D); break; // SET 3, D + case 0xDB: BIT_OP(SET, 3, E); break; // SET 3, E + case 0xDC: BIT_OP(SET, 3, H); break; // SET 3, H + case 0xDD: BIT_OP(SET, 3, L); break; // SET 3, L + case 0xDE: BIT_OP_IND(SET, 3, L, H); break; // SET 3, (HL) + case 0xDF: BIT_OP(SET, 3, A); break; // SET 3, A + case 0xE0: BIT_OP(SET, 4, B); break; // SET 4, B + case 0xE1: BIT_OP(SET, 4, C); break; // SET 4, C + case 0xE2: BIT_OP(SET, 4, D); break; // SET 4, D + case 0xE3: BIT_OP(SET, 4, E); break; // SET 4, E + case 0xE4: BIT_OP(SET, 4, H); break; // SET 4, H + case 0xE5: BIT_OP(SET, 4, L); break; // SET 4, L + case 0xE6: BIT_OP_IND(SET, 4, L, H); break; // SET 4, (HL) + case 0xE7: BIT_OP(SET, 4, A); break; // SET 4, A + case 0xE8: BIT_OP(SET, 5, B); break; // SET 5, B + case 0xE9: BIT_OP(SET, 5, C); break; // SET 5, C + case 0xEA: BIT_OP(SET, 5, D); break; // SET 5, D + case 0xEB: BIT_OP(SET, 5, E); break; // SET 5, E + case 0xEC: BIT_OP(SET, 5, H); break; // SET 5, H + case 0xED: BIT_OP(SET, 5, L); break; // SET 5, L + case 0xEE: BIT_OP_IND(SET, 5, L, H); break; // SET 5, (HL) + case 0xEF: BIT_OP(SET, 5, A); break; // SET 5, A + case 0xF0: BIT_OP(SET, 6, B); break; // SET 6, B + case 0xF1: BIT_OP(SET, 6, C); break; // SET 6, C + case 0xF2: BIT_OP(SET, 6, D); break; // SET 6, D + case 0xF3: BIT_OP(SET, 6, E); break; // SET 6, E + case 0xF4: BIT_OP(SET, 6, H); break; // SET 6, H + case 0xF5: BIT_OP(SET, 6, L); break; // SET 6, L + case 0xF6: BIT_OP_IND(SET, 6, L, H); break; // SET 6, (HL) + case 0xF7: BIT_OP(SET, 6, A); break; // SET 6, A + case 0xF8: BIT_OP(SET, 7, B); break; // SET 7, B + case 0xF9: BIT_OP(SET, 7, C); break; // SET 7, C + case 0xFA: BIT_OP(SET, 7, D); break; // SET 7, D + case 0xFB: BIT_OP(SET, 7, E); break; // SET 7, E + case 0xFC: BIT_OP(SET, 7, H); break; // SET 7, H + case 0xFD: BIT_OP(SET, 7, L); break; // SET 7, L + case 0xFE: BIT_OP_IND(SET, 7, L, H); break; // SET 7, (HL) + case 0xFF: BIT_OP(SET, 7, A); break; // SET 7, A + } + } + else if (EXTD_prefix) + { + // NOTE: Much of EXTD is empty + EXTD_prefix = false; + NO_prefix = true; + + switch (opcode) + { + case 0x40: IN_REG_(B, C); break; // IN B, (C) + case 0x41: OUT_REG_(C, B); break; // OUT (C), B + case 0x42: REG_OP_16_(SBC16, L, H, C, B); break; // SBC HL, BC + case 0x43: LD_16_IND_nn(C, B); break; // LD (nn), BC + case 0x44: INT_OP(NEG, A); break; // NEG + case 0x45: RETN_(); break; // RETN + case 0x46: INT_MODE_(0); break; // IM $0 + case 0x47: REG_OP_IR(TR, I, A); break; // LD I, A + case 0x48: IN_REG_(C, C); break; // IN C, (C) + case 0x49: OUT_REG_(C, C); break; // OUT (C), C + case 0x4A: REG_OP_16_(ADC16, L, H, C, B); break; // ADC HL, BC + case 0x4B: LD_IND_16_nn(C, B); break; // LD BC, (nn) + case 0x4C: INT_OP(NEG, A); break; // NEG + case 0x4D: RETI_(); break; // RETI + case 0x4E: INT_MODE_(0); break; // IM $0 + case 0x4F: REG_OP_IR(TR, R, A); break; // LD R, A + case 0x50: IN_REG_(D, C); break; // IN D, (C) + case 0x51: OUT_REG_(C, D); break; // OUT (C), D + case 0x52: REG_OP_16_(SBC16, L, H, E, D); break; // SBC HL, DE + case 0x53: LD_16_IND_nn(E, D); break; // LD (nn), DE + case 0x54: INT_OP(NEG, A); break; // NEG + case 0x55: RETN_(); break; // RETN + case 0x56: INT_MODE_(1); break; // IM $1 + case 0x57: REG_OP_IR(TR, A, I); break; // LD A, I + case 0x58: IN_REG_(E, C); break; // IN E, (C) + case 0x59: OUT_REG_(C, E); break; // OUT (C), E + case 0x5A: REG_OP_16_(ADC16, L, H, E, D); break; // ADC HL, DE + case 0x5B: LD_IND_16_nn(E, D); break; // LD DE, (nn) + case 0x5C: INT_OP(NEG, A); break; // NEG + case 0x5D: RETN_(); break; // RETI + case 0x5E: INT_MODE_(2); break; // IM $0 + case 0x5F: REG_OP_IR(TR, A, R); break; // LD A, R + case 0x60: IN_REG_(H, C); break; // IN H, (C) + case 0x61: OUT_REG_(C, H); break; // OUT (C), H + case 0x62: REG_OP_16_(SBC16, L, H, L, H); break; // SBC HL, HL + case 0x63: LD_16_IND_nn(L, H); break; // LD (nn), HL + case 0x64: INT_OP(NEG, A); break; // NEG + case 0x65: RETN_(); break; // RETN + case 0x66: INT_MODE_(0); break; // IM $0 + case 0x67: RRD_(); break; // RRD + case 0x68: IN_REG_(L, C); break; // IN L, (C) + case 0x69: OUT_REG_(C, L); break; // OUT (C), L + case 0x6A: REG_OP_16_(ADC16, L, H, L, H); break; // ADC HL, HL + case 0x6B: LD_IND_16_nn(L, H); break; // LD HL, (nn) + case 0x6C: INT_OP(NEG, A); break; // NEG + case 0x6D: RETN_(); break; // RETI + case 0x6E: INT_MODE_(0); break; // IM $0 + case 0x6F: RLD_(); break; // LD R, A + case 0x70: IN_REG_(ALU, C); break; // IN 0, (C) + case 0x71: OUT_REG_(C, ZERO); break; // OUT (C), 0 + case 0x72: REG_OP_16_(SBC16, L, H, SPl, SPh); break; // SBC HL, SP + case 0x73: LD_16_IND_nn(SPl, SPh); break; // LD (nn), SP + case 0x74: INT_OP(NEG, A); break; // NEG + case 0x75: RETN_(); break; // RETN + case 0x76: INT_MODE_(1); break; // IM $1 + case 0x77: NOP_(); break; // NOP + case 0x78: IN_REG_(A, C); break; // IN A, (C) + case 0x79: OUT_REG_(C, A); break; // OUT (C), A + case 0x7A: REG_OP_16_(ADC16, L, H, SPl, SPh); break; // ADC HL, SP + case 0x7B: LD_IND_16_nn(SPl, SPh); break; // LD SP, (nn) + case 0x7C: INT_OP(NEG, A); break; // NEG + case 0x7D: RETN_(); break; // RETI + case 0x7E: INT_MODE_(2); break; // IM $2 + case 0x7F: NOP_(); break; // NOP + case 0xA0: LD_OP_R(INC16, 0); break; // LDI + case 0xA1: CP_OP_R(INC16, 0); break; // CPI + case 0xA2: IN_OP_R(INC16, 0); break; // INI + case 0xA3: OUT_OP_R(INC16, 0); break; // OUTI + case 0xA8: LD_OP_R(DEC16, 0); break; // LDD + case 0xA9: CP_OP_R(DEC16, 0); break; // CPD + case 0xAA: IN_OP_R(DEC16, 0); break; // IND + case 0xAB: OUT_OP_R(DEC16, 0); break; // OUTD + case 0xB0: LD_OP_R(INC16, 1); break; // LDIR + case 0xB1: CP_OP_R(INC16, 1); break; // CPIR + case 0xB2: IN_OP_R(INC16, 1); break; // INIR + case 0xB3: OUT_OP_R(INC16, 1); break; // OTIR + case 0xB8: LD_OP_R(DEC16, 1); break; // LDDR + case 0xB9: CP_OP_R(DEC16, 1); break; // CPDR + case 0xBA: IN_OP_R(DEC16, 1); break; // INDR + case 0xBB: OUT_OP_R(DEC16, 1); break; // OTDR + default: NOP_(); break; // NOP + } + } + else if (IX_prefix) + { + IX_prefix = false; + NO_prefix = true; + + switch (opcode) + { + case 0x00: NOP_(); break; // NOP + case 0x01: LD_IND_16(C, B, PCl, PCh); break; // LD BC, nn + case 0x02: LD_8_IND(C, B, A); break; // LD (BC), A + case 0x03: INC_16(C, B); break; // INC BC + case 0x04: INT_OP(INC8, B); break; // INC B + case 0x05: INT_OP(DEC8, B); break; // DEC B + case 0x06: LD_IND_8_INC(B, PCl, PCh); break; // LD B, n + case 0x07: INT_OP(RLC, Aim); break; // RLCA + case 0x08: EXCH_(); break; // EXCH AF, AF' + case 0x09: ADD_16(Ixl, Ixh, C, B); break; // ADD Ix, BC + case 0x0A: REG_OP_IND(TR, A, C, B); break; // LD A, (BC) + case 0x0B: DEC_16(C, B); break; // DEC BC + case 0x0C: INT_OP(INC8, C); break; // INC C + case 0x0D: INT_OP(DEC8, C); break; // DEC C + case 0x0E: LD_IND_8_INC(C, PCl, PCh); break; // LD C, n + case 0x0F: INT_OP(RRC, Aim); break; // RRCA + case 0x10: DJNZ_(); break; // DJNZ B + case 0x11: LD_IND_16(E, D, PCl, PCh); break; // LD DE, nn + case 0x12: LD_8_IND(E, D, A); break; // LD (DE), A + case 0x13: INC_16(E, D); break; // INC DE + case 0x14: INT_OP(INC8, D); break; // INC D + case 0x15: INT_OP(DEC8, D); break; // DEC D + case 0x16: LD_IND_8_INC(D, PCl, PCh); break; // LD D, n + case 0x17: INT_OP(RL, Aim); break; // RLA + case 0x18: JR_COND(true); break; // JR, r8 + case 0x19: ADD_16(Ixl, Ixh, E, D); break; // ADD Ix, DE + case 0x1A: REG_OP_IND(TR, A, E, D); break; // LD A, (DE) + case 0x1B: DEC_16(E, D); break; // DEC DE + case 0x1C: INT_OP(INC8, E); break; // INC E + case 0x1D: INT_OP(DEC8, E); break; // DEC E + case 0x1E: LD_IND_8_INC(E, PCl, PCh); break; // LD E, n + case 0x1F: INT_OP(RR, Aim); break; // RRA + case 0x20: JR_COND(!FlagZ); break; // JR NZ, r8 + case 0x21: LD_IND_16(Ixl, Ixh, PCl, PCh); break; // LD Ix, nn + case 0x22: LD_16_IND_nn(Ixl, Ixh); break; // LD (nn), Ix + case 0x23: INC_16(Ixl, Ixh); break; // INC Ix + case 0x24: INT_OP(INC8, Ixh); break; // INC Ixh + case 0x25: INT_OP(DEC8, Ixh); break; // DEC Ixh + case 0x26: LD_IND_8_INC(Ixh, PCl, PCh); break; // LD Ixh, n + case 0x27: INT_OP(DA, A); break; // DAA + case 0x28: JR_COND(FlagZ); break; // JR Z, r8 + case 0x29: ADD_16(Ixl, Ixh, Ixl, Ixh); break; // ADD Ix, Ix + case 0x2A: LD_IND_16_nn(Ixl, Ixh); break; // LD Ix, (nn) + case 0x2B: DEC_16(Ixl, Ixh); break; // DEC Ix + case 0x2C: INT_OP(INC8, Ixl); break; // INC Ixl + case 0x2D: INT_OP(DEC8, Ixl); break; // DEC Ixl + case 0x2E: LD_IND_8_INC(Ixl, PCl, PCh); break; // LD Ixl, n + case 0x2F: INT_OP(CPL, A); break; // CPL + case 0x30: JR_COND(!FlagC); break; // JR NC, r8 + case 0x31: LD_IND_16(SPl, SPh, PCl, PCh); break; // LD SP, nn + case 0x32: LD_8_IND_nn(A); break; // LD (nn), A + case 0x33: INC_16(SPl, SPh); break; // INC SP + case 0x34: I_OP_n(INC8, Ixl, Ixh); break; // INC (Ix + n) + case 0x35: I_OP_n(DEC8, Ixl, Ixh); break; // DEC (Ix + n) + case 0x36: I_OP_n_n(Ixl, Ixh); break; // LD (Ix + n), n + case 0x37: INT_OP(SCF, A); break; // SCF + case 0x38: JR_COND(FlagC); break; // JR C, r8 + case 0x39: ADD_16(Ixl, Ixh, SPl, SPh); break; // ADD Ix, SP + case 0x3A: LD_IND_8_nn(A); break; // LD A, (nn) + case 0x3B: DEC_16(SPl, SPh); break; // DEC SP + case 0x3C: INT_OP(INC8, A); break; // INC A + case 0x3D: INT_OP(DEC8, A); break; // DEC A + case 0x3E: LD_IND_8_INC(A, PCl, PCh); break; // LD A, n + case 0x3F: INT_OP(CCF, A); break; // CCF + case 0x40: REG_OP(TR, B, B); break; // LD B, B + case 0x41: REG_OP(TR, B, C); break; // LD B, C + case 0x42: REG_OP(TR, B, D); break; // LD B, D + case 0x43: REG_OP(TR, B, E); break; // LD B, E + case 0x44: REG_OP(TR, B, Ixh); break; // LD B, Ixh + case 0x45: REG_OP(TR, B, Ixl); break; // LD B, Ixl + case 0x46: I_REG_OP_IND_n(TR, B, Ixl, Ixh); break; // LD B, (Ix + n) + case 0x47: REG_OP(TR, B, A); break; // LD B, A + case 0x48: REG_OP(TR, C, B); break; // LD C, B + case 0x49: REG_OP(TR, C, C); break; // LD C, C + case 0x4A: REG_OP(TR, C, D); break; // LD C, D + case 0x4B: REG_OP(TR, C, E); break; // LD C, E + case 0x4C: REG_OP(TR, C, Ixh); break; // LD C, Ixh + case 0x4D: REG_OP(TR, C, Ixl); break; // LD C, Ixl + case 0x4E: I_REG_OP_IND_n(TR, C, Ixl, Ixh); break; // LD C, (Ix + n) + case 0x4F: REG_OP(TR, C, A); break; // LD C, A + case 0x50: REG_OP(TR, D, B); break; // LD D, B + case 0x51: REG_OP(TR, D, C); break; // LD D, C + case 0x52: REG_OP(TR, D, D); break; // LD D, D + case 0x53: REG_OP(TR, D, E); break; // LD D, E + case 0x54: REG_OP(TR, D, Ixh); break; // LD D, Ixh + case 0x55: REG_OP(TR, D, Ixl); break; // LD D, Ixl + case 0x56: I_REG_OP_IND_n(TR, D, Ixl, Ixh); break; // LD D, (Ix + n) + case 0x57: REG_OP(TR, D, A); break; // LD D, A + case 0x58: REG_OP(TR, E, B); break; // LD E, B + case 0x59: REG_OP(TR, E, C); break; // LD E, C + case 0x5A: REG_OP(TR, E, D); break; // LD E, D + case 0x5B: REG_OP(TR, E, E); break; // LD E, E + case 0x5C: REG_OP(TR, E, Ixh); break; // LD E, Ixh + case 0x5D: REG_OP(TR, E, Ixl); break; // LD E, Ixl + case 0x5E: I_REG_OP_IND_n(TR, E, Ixl, Ixh); break; // LD E, (Ix + n) + case 0x5F: REG_OP(TR, E, A); break; // LD E, A + case 0x60: REG_OP(TR, Ixh, B); break; // LD Ixh, B + case 0x61: REG_OP(TR, Ixh, C); break; // LD Ixh, C + case 0x62: REG_OP(TR, Ixh, D); break; // LD Ixh, D + case 0x63: REG_OP(TR, Ixh, E); break; // LD Ixh, E + case 0x64: REG_OP(TR, Ixh, Ixh); break; // LD Ixh, Ixh + case 0x65: REG_OP(TR, Ixh, Ixl); break; // LD Ixh, Ixl + case 0x66: I_REG_OP_IND_n(TR, H, Ixl, Ixh); break; // LD H, (Ix + n) + case 0x67: REG_OP(TR, Ixh, A); break; // LD Ixh, A + case 0x68: REG_OP(TR, Ixl, B); break; // LD Ixl, B + case 0x69: REG_OP(TR, Ixl, C); break; // LD Ixl, C + case 0x6A: REG_OP(TR, Ixl, D); break; // LD Ixl, D + case 0x6B: REG_OP(TR, Ixl, E); break; // LD Ixl, E + case 0x6C: REG_OP(TR, Ixl, Ixh); break; // LD Ixl, Ixh + case 0x6D: REG_OP(TR, Ixl, Ixl); break; // LD Ixl, Ixl + case 0x6E: I_REG_OP_IND_n(TR, L, Ixl, Ixh); break; // LD L, (Ix + n) + case 0x6F: REG_OP(TR, Ixl, A); break; // LD Ixl, A + case 0x70: I_LD_8_IND_n(Ixl, Ixh, B); break; // LD (Ix + n), B + case 0x71: I_LD_8_IND_n(Ixl, Ixh, C); break; // LD (Ix + n), C + case 0x72: I_LD_8_IND_n(Ixl, Ixh, D); break; // LD (Ix + n), D + case 0x73: I_LD_8_IND_n(Ixl, Ixh, E); break; // LD (Ix + n), E + case 0x74: I_LD_8_IND_n(Ixl, Ixh, H); break; // LD (Ix + n), H + case 0x75: I_LD_8_IND_n(Ixl, Ixh, L); break; // LD (Ix + n), L + case 0x76: HALT_(); break; // HALT + case 0x77: I_LD_8_IND_n(Ixl, Ixh, A); break; // LD (Ix + n), A + case 0x78: REG_OP(TR, A, B); break; // LD A, B + case 0x79: REG_OP(TR, A, C); break; // LD A, C + case 0x7A: REG_OP(TR, A, D); break; // LD A, D + case 0x7B: REG_OP(TR, A, E); break; // LD A, E + case 0x7C: REG_OP(TR, A, Ixh); break; // LD A, Ixh + case 0x7D: REG_OP(TR, A, Ixl); break; // LD A, Ixl + case 0x7E: I_REG_OP_IND_n(TR, A, Ixl, Ixh); break; // LD A, (Ix + n) + case 0x7F: REG_OP(TR, A, A); break; // LD A, A + case 0x80: REG_OP(ADD8, A, B); break; // ADD A, B + case 0x81: REG_OP(ADD8, A, C); break; // ADD A, C + case 0x82: REG_OP(ADD8, A, D); break; // ADD A, D + case 0x83: REG_OP(ADD8, A, E); break; // ADD A, E + case 0x84: REG_OP(ADD8, A, Ixh); break; // ADD A, Ixh + case 0x85: REG_OP(ADD8, A, Ixl); break; // ADD A, Ixl + case 0x86: I_REG_OP_IND_n(ADD8, A, Ixl, Ixh); break; // ADD A, (Ix + n) + case 0x87: REG_OP(ADD8, A, A); break; // ADD A, A + case 0x88: REG_OP(ADC8, A, B); break; // ADC A, B + case 0x89: REG_OP(ADC8, A, C); break; // ADC A, C + case 0x8A: REG_OP(ADC8, A, D); break; // ADC A, D + case 0x8B: REG_OP(ADC8, A, E); break; // ADC A, E + case 0x8C: REG_OP(ADC8, A, Ixh); break; // ADC A, Ixh + case 0x8D: REG_OP(ADC8, A, Ixl); break; // ADC A, Ixl + case 0x8E: I_REG_OP_IND_n(ADC8, A, Ixl, Ixh); break; // ADC A, (Ix + n) + case 0x8F: REG_OP(ADC8, A, A); break; // ADC A, A + case 0x90: REG_OP(SUB8, A, B); break; // SUB A, B + case 0x91: REG_OP(SUB8, A, C); break; // SUB A, C + case 0x92: REG_OP(SUB8, A, D); break; // SUB A, D + case 0x93: REG_OP(SUB8, A, E); break; // SUB A, E + case 0x94: REG_OP(SUB8, A, Ixh); break; // SUB A, Ixh + case 0x95: REG_OP(SUB8, A, Ixl); break; // SUB A, Ixl + case 0x96: I_REG_OP_IND_n(SUB8, A, Ixl, Ixh); break; // SUB A, (Ix + n) + case 0x97: REG_OP(SUB8, A, A); break; // SUB A, A + case 0x98: REG_OP(SBC8, A, B); break; // SBC A, B + case 0x99: REG_OP(SBC8, A, C); break; // SBC A, C + case 0x9A: REG_OP(SBC8, A, D); break; // SBC A, D + case 0x9B: REG_OP(SBC8, A, E); break; // SBC A, E + case 0x9C: REG_OP(SBC8, A, Ixh); break; // SBC A, Ixh + case 0x9D: REG_OP(SBC8, A, Ixl); break; // SBC A, Ixl + case 0x9E: I_REG_OP_IND_n(SBC8, A, Ixl, Ixh); break; // SBC A, (Ix + n) + case 0x9F: REG_OP(SBC8, A, A); break; // SBC A, A + case 0xA0: REG_OP(AND8, A, B); break; // AND A, B + case 0xA1: REG_OP(AND8, A, C); break; // AND A, C + case 0xA2: REG_OP(AND8, A, D); break; // AND A, D + case 0xA3: REG_OP(AND8, A, E); break; // AND A, E + case 0xA4: REG_OP(AND8, A, Ixh); break; // AND A, Ixh + case 0xA5: REG_OP(AND8, A, Ixl); break; // AND A, Ixl + case 0xA6: I_REG_OP_IND_n(AND8, A, Ixl, Ixh); break; // AND A, (Ix + n) + case 0xA7: REG_OP(AND8, A, A); break; // AND A, A + case 0xA8: REG_OP(XOR8, A, B); break; // XOR A, B + case 0xA9: REG_OP(XOR8, A, C); break; // XOR A, C + case 0xAA: REG_OP(XOR8, A, D); break; // XOR A, D + case 0xAB: REG_OP(XOR8, A, E); break; // XOR A, E + case 0xAC: REG_OP(XOR8, A, Ixh); break; // XOR A, Ixh + case 0xAD: REG_OP(XOR8, A, Ixl); break; // XOR A, Ixl + case 0xAE: I_REG_OP_IND_n(XOR8, A, Ixl, Ixh); break; // XOR A, (Ix + n) + case 0xAF: REG_OP(XOR8, A, A); break; // XOR A, A + case 0xB0: REG_OP(OR8, A, B); break; // OR A, B + case 0xB1: REG_OP(OR8, A, C); break; // OR A, C + case 0xB2: REG_OP(OR8, A, D); break; // OR A, D + case 0xB3: REG_OP(OR8, A, E); break; // OR A, E + case 0xB4: REG_OP(OR8, A, Ixh); break; // OR A, Ixh + case 0xB5: REG_OP(OR8, A, Ixl); break; // OR A, Ixl + case 0xB6: I_REG_OP_IND_n(OR8, A, Ixl, Ixh); break; // OR A, (Ix + n) + case 0xB7: REG_OP(OR8, A, A); break; // OR A, A + case 0xB8: REG_OP(CP8, A, B); break; // CP A, B + case 0xB9: REG_OP(CP8, A, C); break; // CP A, C + case 0xBA: REG_OP(CP8, A, D); break; // CP A, D + case 0xBB: REG_OP(CP8, A, E); break; // CP A, E + case 0xBC: REG_OP(CP8, A, Ixh); break; // CP A, Ixh + case 0xBD: REG_OP(CP8, A, Ixl); break; // CP A, Ixl + case 0xBE: I_REG_OP_IND_n(CP8, A, Ixl, Ixh); break; // CP A, (Ix + n) + case 0xBF: REG_OP(CP8, A, A); break; // CP A, A + case 0xC0: RET_COND(!FlagZ); break; // Ret NZ + case 0xC1: POP_(C, B); break; // POP BC + case 0xC2: JP_COND(!FlagZ); break; // JP NZ + case 0xC3: JP_COND(true); break; // JP + case 0xC4: CALL_COND(!FlagZ); break; // CALL NZ + case 0xC5: PUSH_(C, B); break; // PUSH BC + case 0xC6: REG_OP_IND_INC(ADD8, A, PCl, PCh); break; // ADD A, n + case 0xC7: RST_(0); break; // RST 0 + case 0xC8: RET_COND(FlagZ); break; // RET Z + case 0xC9: RET_(); break; // RET + case 0xCA: JP_COND(FlagZ); break; // JP Z + case 0xCB: PREFETCH_(IXCBpre); break; // PREFIX IXCB + case 0xCC: CALL_COND(FlagZ); break; // CALL Z + case 0xCD: CALL_COND(true); break; // CALL + case 0xCE: REG_OP_IND_INC(ADC8, A, PCl, PCh); break; // ADC A, n + case 0xCF: RST_(0x08); break; // RST 0x08 + case 0xD0: RET_COND(!FlagC); break; // Ret NC + case 0xD1: POP_(E, D); break; // POP DE + case 0xD2: JP_COND(!FlagC); break; // JP NC + case 0xD3: OUT_(); break; // OUT A + case 0xD4: CALL_COND(!FlagC); break; // CALL NC + case 0xD5: PUSH_(E, D); break; // PUSH DE + case 0xD6: REG_OP_IND_INC(SUB8, A, PCl, PCh); break; // SUB A, n + case 0xD7: RST_(0x10); break; // RST 0x10 + case 0xD8: RET_COND(FlagC); break; // RET C + case 0xD9: EXX_(); break; // EXX + case 0xDA: JP_COND(FlagC); break; // JP C + case 0xDB: IN_(); break; // IN A + case 0xDC: CALL_COND(FlagC); break; // CALL C + case 0xDD: PREFIX_(IXpre); break; // IX Prefix + case 0xDE: REG_OP_IND_INC(SBC8, A, PCl, PCh); break; // SBC A, n + case 0xDF: RST_(0x18); break; // RST 0x18 + case 0xE0: RET_COND(!FlagP); break; // RET Po + case 0xE1: POP_(Ixl, Ixh); break; // POP Ix + case 0xE2: JP_COND(!FlagP); break; // JP Po + case 0xE3: EXCH_16_IND_(SPl, SPh, Ixl, Ixh); break; // ex (SP), Ix + case 0xE4: CALL_COND(!FlagP); break; // CALL Po + case 0xE5: PUSH_(Ixl, Ixh); break; // PUSH Ix + case 0xE6: REG_OP_IND_INC(AND8, A, PCl, PCh); break; // AND A, n + case 0xE7: RST_(0x20); break; // RST 0x20 + case 0xE8: RET_COND(FlagP); break; // RET Pe + case 0xE9: JP_16(Ixl, Ixh); break; // JP (Ix) + case 0xEA: JP_COND(FlagP); break; // JP Pe + case 0xEB: EXCH_16_(E, D, L, H); break; // ex DE, HL + case 0xEC: CALL_COND(FlagP); break; // CALL Pe + case 0xED: PREFIX_(EXTDpre); break; // EXTD Prefix + case 0xEE: REG_OP_IND_INC(XOR8, A, PCl, PCh); break; // XOR A, n + case 0xEF: RST_(0x28); break; // RST 0x28 + case 0xF0: RET_COND(!FlagS); break; // RET p + case 0xF1: POP_(F, A); break; // POP AF + case 0xF2: JP_COND(!FlagS); break; // JP p + case 0xF3: DI_(); break; // DI + case 0xF4: CALL_COND(!FlagS); break; // CALL p + case 0xF5: PUSH_(F, A); break; // PUSH AF + case 0xF6: REG_OP_IND_INC(OR8, A, PCl, PCh); break; // OR A, n + case 0xF7: RST_(0x30); break; // RST 0x30 + case 0xF8: RET_COND(FlagS); break; // RET M + case 0xF9: LD_SP_16(Ixl, Ixh); break; // LD SP, Ix + case 0xFA: JP_COND(FlagS); break; // JP M + case 0xFB: EI_(); break; // EI + case 0xFC: CALL_COND(FlagS); break; // CALL M + case 0xFD: PREFIX_(IYpre); break; // IY Prefix + case 0xFE: REG_OP_IND_INC(CP8, A, PCl, PCh); break; // CP A, n + case 0xFF: RST_(0x38); break; // RST $38 + } + } + else if (IY_prefix) + { + IY_prefix = false; + NO_prefix = true; + + switch (opcode) + { + case 0x00: NOP_(); break; // NOP + case 0x01: LD_IND_16(C, B, PCl, PCh); break; // LD BC, nn + case 0x02: LD_8_IND(C, B, A); break; // LD (BC), A + case 0x03: INC_16(C, B); break; // INC BC + case 0x04: INT_OP(INC8, B); break; // INC B + case 0x05: INT_OP(DEC8, B); break; // DEC B + case 0x06: LD_IND_8_INC(B, PCl, PCh); break; // LD B, n + case 0x07: INT_OP(RLC, Aim); break; // RLCA + case 0x08: EXCH_(); break; // EXCH AF, AF' + case 0x09: ADD_16(Iyl, Iyh, C, B); break; // ADD Iy, BC + case 0x0A: REG_OP_IND(TR, A, C, B); break; // LD A, (BC) + case 0x0B: DEC_16(C, B); break; // DEC BC + case 0x0C: INT_OP(INC8, C); break; // INC C + case 0x0D: INT_OP(DEC8, C); break; // DEC C + case 0x0E: LD_IND_8_INC(C, PCl, PCh); break; // LD C, n + case 0x0F: INT_OP(RRC, Aim); break; // RRCA + case 0x10: DJNZ_(); break; // DJNZ B + case 0x11: LD_IND_16(E, D, PCl, PCh); break; // LD DE, nn + case 0x12: LD_8_IND(E, D, A); break; // LD (DE), A + case 0x13: INC_16(E, D); break; // INC DE + case 0x14: INT_OP(INC8, D); break; // INC D + case 0x15: INT_OP(DEC8, D); break; // DEC D + case 0x16: LD_IND_8_INC(D, PCl, PCh); break; // LD D, n + case 0x17: INT_OP(RL, Aim); break; // RLA + case 0x18: JR_COND(true); break; // JR, r8 + case 0x19: ADD_16(Iyl, Iyh, E, D); break; // ADD Iy, DE + case 0x1A: REG_OP_IND(TR, A, E, D); break; // LD A, (DE) + case 0x1B: DEC_16(E, D); break; // DEC DE + case 0x1C: INT_OP(INC8, E); break; // INC E + case 0x1D: INT_OP(DEC8, E); break; // DEC E + case 0x1E: LD_IND_8_INC(E, PCl, PCh); break; // LD E, n + case 0x1F: INT_OP(RR, Aim); break; // RRA + case 0x20: JR_COND(!FlagZ); break; // JR NZ, r8 + case 0x21: LD_IND_16(Iyl, Iyh, PCl, PCh); break; // LD Iy, nn + case 0x22: LD_16_IND_nn(Iyl, Iyh); break; // LD (nn), Iy + case 0x23: INC_16(Iyl, Iyh); break; // INC Iy + case 0x24: INT_OP(INC8, Iyh); break; // INC Iyh + case 0x25: INT_OP(DEC8, Iyh); break; // DEC Iyh + case 0x26: LD_IND_8_INC(Iyh, PCl, PCh); break; // LD Iyh, n + case 0x27: INT_OP(DA, A); break; // DAA + case 0x28: JR_COND(FlagZ); break; // JR Z, r8 + case 0x29: ADD_16(Iyl, Iyh, Iyl, Iyh); break; // ADD Iy, Iy + case 0x2A: LD_IND_16_nn(Iyl, Iyh); break; // LD Iy, (nn) + case 0x2B: DEC_16(Iyl, Iyh); break; // DEC Iy + case 0x2C: INT_OP(INC8, Iyl); break; // INC Iyl + case 0x2D: INT_OP(DEC8, Iyl); break; // DEC Iyl + case 0x2E: LD_IND_8_INC(Iyl, PCl, PCh); break; // LD Iyl, n + case 0x2F: INT_OP(CPL, A); break; // CPL + case 0x30: JR_COND(!FlagC); break; // JR NC, r8 + case 0x31: LD_IND_16(SPl, SPh, PCl, PCh); break; // LD SP, nn + case 0x32: LD_8_IND_nn(A); break; // LD (nn), A + case 0x33: INC_16(SPl, SPh); break; // INC SP + case 0x34: I_OP_n(INC8, Iyl, Iyh); break; // INC (Iy + n) + case 0x35: I_OP_n(DEC8, Iyl, Iyh); break; // DEC (Iy + n) + case 0x36: I_OP_n_n(Iyl, Iyh); break; // LD (Iy + n), n + case 0x37: INT_OP(SCF, A); break; // SCF + case 0x38: JR_COND(FlagC); break; // JR C, r8 + case 0x39: ADD_16(Iyl, Iyh, SPl, SPh); break; // ADD Iy, SP + case 0x3A: LD_IND_8_nn(A); break; // LD A, (nn) + case 0x3B: DEC_16(SPl, SPh); break; // DEC SP + case 0x3C: INT_OP(INC8, A); break; // INC A + case 0x3D: INT_OP(DEC8, A); break; // DEC A + case 0x3E: LD_IND_8_INC(A, PCl, PCh); break; // LD A, n + case 0x3F: INT_OP(CCF, A); break; // CCF + case 0x40: REG_OP(TR, B, B); break; // LD B, B + case 0x41: REG_OP(TR, B, C); break; // LD B, C + case 0x42: REG_OP(TR, B, D); break; // LD B, D + case 0x43: REG_OP(TR, B, E); break; // LD B, E + case 0x44: REG_OP(TR, B, Iyh); break; // LD B, Iyh + case 0x45: REG_OP(TR, B, Iyl); break; // LD B, Iyl + case 0x46: I_REG_OP_IND_n(TR, B, Iyl, Iyh); break; // LD B, (Iy + n) + case 0x47: REG_OP(TR, B, A); break; // LD B, A + case 0x48: REG_OP(TR, C, B); break; // LD C, B + case 0x49: REG_OP(TR, C, C); break; // LD C, C + case 0x4A: REG_OP(TR, C, D); break; // LD C, D + case 0x4B: REG_OP(TR, C, E); break; // LD C, E + case 0x4C: REG_OP(TR, C, Iyh); break; // LD C, Iyh + case 0x4D: REG_OP(TR, C, Iyl); break; // LD C, Iyl + case 0x4E: I_REG_OP_IND_n(TR, C, Iyl, Iyh); break; // LD C, (Iy + n) + case 0x4F: REG_OP(TR, C, A); break; // LD C, A + case 0x50: REG_OP(TR, D, B); break; // LD D, B + case 0x51: REG_OP(TR, D, C); break; // LD D, C + case 0x52: REG_OP(TR, D, D); break; // LD D, D + case 0x53: REG_OP(TR, D, E); break; // LD D, E + case 0x54: REG_OP(TR, D, Iyh); break; // LD D, Iyh + case 0x55: REG_OP(TR, D, Iyl); break; // LD D, Iyl + case 0x56: I_REG_OP_IND_n(TR, D, Iyl, Iyh); break; // LD D, (Iy + n) + case 0x57: REG_OP(TR, D, A); break; // LD D, A + case 0x58: REG_OP(TR, E, B); break; // LD E, B + case 0x59: REG_OP(TR, E, C); break; // LD E, C + case 0x5A: REG_OP(TR, E, D); break; // LD E, D + case 0x5B: REG_OP(TR, E, E); break; // LD E, E + case 0x5C: REG_OP(TR, E, Iyh); break; // LD E, Iyh + case 0x5D: REG_OP(TR, E, Iyl); break; // LD E, Iyl + case 0x5E: I_REG_OP_IND_n(TR, E, Iyl, Iyh); break; // LD E, (Iy + n) + case 0x5F: REG_OP(TR, E, A); break; // LD E, A + case 0x60: REG_OP(TR, Iyh, B); break; // LD Iyh, B + case 0x61: REG_OP(TR, Iyh, C); break; // LD Iyh, C + case 0x62: REG_OP(TR, Iyh, D); break; // LD Iyh, D + case 0x63: REG_OP(TR, Iyh, E); break; // LD Iyh, E + case 0x64: REG_OP(TR, Iyh, Iyh); break; // LD Iyh, Iyh + case 0x65: REG_OP(TR, Iyh, Iyl); break; // LD Iyh, Iyl + case 0x66: I_REG_OP_IND_n(TR, H, Iyl, Iyh); break; // LD H, (Iy + n) + case 0x67: REG_OP(TR, Iyh, A); break; // LD Iyh, A + case 0x68: REG_OP(TR, Iyl, B); break; // LD Iyl, B + case 0x69: REG_OP(TR, Iyl, C); break; // LD Iyl, C + case 0x6A: REG_OP(TR, Iyl, D); break; // LD Iyl, D + case 0x6B: REG_OP(TR, Iyl, E); break; // LD Iyl, E + case 0x6C: REG_OP(TR, Iyl, Iyh); break; // LD Iyl, Iyh + case 0x6D: REG_OP(TR, Iyl, Iyl); break; // LD Iyl, Iyl + case 0x6E: I_REG_OP_IND_n(TR, L, Iyl, Iyh); break; // LD L, (Iy + n) + case 0x6F: REG_OP(TR, Iyl, A); break; // LD Iyl, A + case 0x70: I_LD_8_IND_n(Iyl, Iyh, B); break; // LD (Iy + n), B + case 0x71: I_LD_8_IND_n(Iyl, Iyh, C); break; // LD (Iy + n), C + case 0x72: I_LD_8_IND_n(Iyl, Iyh, D); break; // LD (Iy + n), D + case 0x73: I_LD_8_IND_n(Iyl, Iyh, E); break; // LD (Iy + n), E + case 0x74: I_LD_8_IND_n(Iyl, Iyh, H); break; // LD (Iy + n), H + case 0x75: I_LD_8_IND_n(Iyl, Iyh, L); break; // LD (Iy + n), L + case 0x76: HALT_(); break; // HALT + case 0x77: I_LD_8_IND_n(Iyl, Iyh, A); break; // LD (Iy + n), A + case 0x78: REG_OP(TR, A, B); break; // LD A, B + case 0x79: REG_OP(TR, A, C); break; // LD A, C + case 0x7A: REG_OP(TR, A, D); break; // LD A, D + case 0x7B: REG_OP(TR, A, E); break; // LD A, E + case 0x7C: REG_OP(TR, A, Iyh); break; // LD A, Iyh + case 0x7D: REG_OP(TR, A, Iyl); break; // LD A, Iyl + case 0x7E: I_REG_OP_IND_n(TR, A, Iyl, Iyh); break; // LD A, (Iy + n) + case 0x7F: REG_OP(TR, A, A); break; // LD A, A + case 0x80: REG_OP(ADD8, A, B); break; // ADD A, B + case 0x81: REG_OP(ADD8, A, C); break; // ADD A, C + case 0x82: REG_OP(ADD8, A, D); break; // ADD A, D + case 0x83: REG_OP(ADD8, A, E); break; // ADD A, E + case 0x84: REG_OP(ADD8, A, Iyh); break; // ADD A, Iyh + case 0x85: REG_OP(ADD8, A, Iyl); break; // ADD A, Iyl + case 0x86: I_REG_OP_IND_n(ADD8, A, Iyl, Iyh); break; // ADD A, (Iy + n) + case 0x87: REG_OP(ADD8, A, A); break; // ADD A, A + case 0x88: REG_OP(ADC8, A, B); break; // ADC A, B + case 0x89: REG_OP(ADC8, A, C); break; // ADC A, C + case 0x8A: REG_OP(ADC8, A, D); break; // ADC A, D + case 0x8B: REG_OP(ADC8, A, E); break; // ADC A, E + case 0x8C: REG_OP(ADC8, A, Iyh); break; // ADC A, Iyh + case 0x8D: REG_OP(ADC8, A, Iyl); break; // ADC A, Iyl + case 0x8E: I_REG_OP_IND_n(ADC8, A, Iyl, Iyh); break; // ADC A, (Iy + n) + case 0x8F: REG_OP(ADC8, A, A); break; // ADC A, A + case 0x90: REG_OP(SUB8, A, B); break; // SUB A, B + case 0x91: REG_OP(SUB8, A, C); break; // SUB A, C + case 0x92: REG_OP(SUB8, A, D); break; // SUB A, D + case 0x93: REG_OP(SUB8, A, E); break; // SUB A, E + case 0x94: REG_OP(SUB8, A, Iyh); break; // SUB A, Iyh + case 0x95: REG_OP(SUB8, A, Iyl); break; // SUB A, Iyl + case 0x96: I_REG_OP_IND_n(SUB8, A, Iyl, Iyh); break; // SUB A, (Iy + n) + case 0x97: REG_OP(SUB8, A, A); break; // SUB A, A + case 0x98: REG_OP(SBC8, A, B); break; // SBC A, B + case 0x99: REG_OP(SBC8, A, C); break; // SBC A, C + case 0x9A: REG_OP(SBC8, A, D); break; // SBC A, D + case 0x9B: REG_OP(SBC8, A, E); break; // SBC A, E + case 0x9C: REG_OP(SBC8, A, Iyh); break; // SBC A, Iyh + case 0x9D: REG_OP(SBC8, A, Iyl); break; // SBC A, Iyl + case 0x9E: I_REG_OP_IND_n(SBC8, A, Iyl, Iyh); break; // SBC A, (Iy + n) + case 0x9F: REG_OP(SBC8, A, A); break; // SBC A, A + case 0xA0: REG_OP(AND8, A, B); break; // AND A, B + case 0xA1: REG_OP(AND8, A, C); break; // AND A, C + case 0xA2: REG_OP(AND8, A, D); break; // AND A, D + case 0xA3: REG_OP(AND8, A, E); break; // AND A, E + case 0xA4: REG_OP(AND8, A, Iyh); break; // AND A, Iyh + case 0xA5: REG_OP(AND8, A, Iyl); break; // AND A, Iyl + case 0xA6: I_REG_OP_IND_n(AND8, A, Iyl, Iyh); break; // AND A, (Iy + n) + case 0xA7: REG_OP(AND8, A, A); break; // AND A, A + case 0xA8: REG_OP(XOR8, A, B); break; // XOR A, B + case 0xA9: REG_OP(XOR8, A, C); break; // XOR A, C + case 0xAA: REG_OP(XOR8, A, D); break; // XOR A, D + case 0xAB: REG_OP(XOR8, A, E); break; // XOR A, E + case 0xAC: REG_OP(XOR8, A, Iyh); break; // XOR A, Iyh + case 0xAD: REG_OP(XOR8, A, Iyl); break; // XOR A, Iyl + case 0xAE: I_REG_OP_IND_n(XOR8, A, Iyl, Iyh); break; // XOR A, (Iy + n) + case 0xAF: REG_OP(XOR8, A, A); break; // XOR A, A + case 0xB0: REG_OP(OR8, A, B); break; // OR A, B + case 0xB1: REG_OP(OR8, A, C); break; // OR A, C + case 0xB2: REG_OP(OR8, A, D); break; // OR A, D + case 0xB3: REG_OP(OR8, A, E); break; // OR A, E + case 0xB4: REG_OP(OR8, A, Iyh); break; // OR A, Iyh + case 0xB5: REG_OP(OR8, A, Iyl); break; // OR A, Iyl + case 0xB6: I_REG_OP_IND_n(OR8, A, Iyl, Iyh); break; // OR A, (Iy + n) + case 0xB7: REG_OP(OR8, A, A); break; // OR A, A + case 0xB8: REG_OP(CP8, A, B); break; // CP A, B + case 0xB9: REG_OP(CP8, A, C); break; // CP A, C + case 0xBA: REG_OP(CP8, A, D); break; // CP A, D + case 0xBB: REG_OP(CP8, A, E); break; // CP A, E + case 0xBC: REG_OP(CP8, A, Iyh); break; // CP A, Iyh + case 0xBD: REG_OP(CP8, A, Iyl); break; // CP A, Iyl + case 0xBE: I_REG_OP_IND_n(CP8, A, Iyl, Iyh); break; // CP A, (Iy + n) + case 0xBF: REG_OP(CP8, A, A); break; // CP A, A + case 0xC0: RET_COND(!FlagZ); break; // Ret NZ + case 0xC1: POP_(C, B); break; // POP BC + case 0xC2: JP_COND(!FlagZ); break; // JP NZ + case 0xC3: JP_COND(true); break; // JP + case 0xC4: CALL_COND(!FlagZ); break; // CALL NZ + case 0xC5: PUSH_(C, B); break; // PUSH BC + case 0xC6: REG_OP_IND_INC(ADD8, A, PCl, PCh); break; // ADD A, n + case 0xC7: RST_(0); break; // RST 0 + case 0xC8: RET_COND(FlagZ); break; // RET Z + case 0xC9: RET_(); break; // RET + case 0xCA: JP_COND(FlagZ); break; // JP Z + case 0xCB: PREFETCH_(IYCBpre); break; // PREFIX IyCB + case 0xCC: CALL_COND(FlagZ); break; // CALL Z + case 0xCD: CALL_COND(true); break; // CALL + case 0xCE: REG_OP_IND_INC(ADC8, A, PCl, PCh); break; // ADC A, n + case 0xCF: RST_(0x08); break; // RST 0x08 + case 0xD0: RET_COND(!FlagC); break; // Ret NC + case 0xD1: POP_(E, D); break; // POP DE + case 0xD2: JP_COND(!FlagC); break; // JP NC + case 0xD3: OUT_(); break; // OUT A + case 0xD4: CALL_COND(!FlagC); break; // CALL NC + case 0xD5: PUSH_(E, D); break; // PUSH DE + case 0xD6: REG_OP_IND_INC(SUB8, A, PCl, PCh); break; // SUB A, n + case 0xD7: RST_(0x10); break; // RST 0x10 + case 0xD8: RET_COND(FlagC); break; // RET C + case 0xD9: EXX_(); break; // EXX + case 0xDA: JP_COND(FlagC); break; // JP C + case 0xDB: IN_(); break; // IN A + case 0xDC: CALL_COND(FlagC); break; // CALL C + case 0xDD: PREFIX_(IXpre); break; // IX Prefix + case 0xDE: REG_OP_IND_INC(SBC8, A, PCl, PCh); break; // SBC A, n + case 0xDF: RST_(0x18); break; // RST 0x18 + case 0xE0: RET_COND(!FlagP); break; // RET Po + case 0xE1: POP_(Iyl, Iyh); break; // POP Iy + case 0xE2: JP_COND(!FlagP); break; // JP Po + case 0xE3: EXCH_16_IND_(SPl, SPh, Iyl, Iyh); break; // ex (SP), Iy + case 0xE4: CALL_COND(!FlagP); break; // CALL Po + case 0xE5: PUSH_(Iyl, Iyh); break; // PUSH Iy + case 0xE6: REG_OP_IND_INC(AND8, A, PCl, PCh); break; // AND A, n + case 0xE7: RST_(0x20); break; // RST 0x20 + case 0xE8: RET_COND(FlagP); break; // RET Pe + case 0xE9: JP_16(Iyl, Iyh); break; // JP (Iy) + case 0xEA: JP_COND(FlagP); break; // JP Pe + case 0xEB: EXCH_16_(E, D, L, H); break; // ex DE, HL + case 0xEC: CALL_COND(FlagP); break; // CALL Pe + case 0xED: PREFIX_(EXTDpre); break; // EXTD Prefix + case 0xEE: REG_OP_IND_INC(XOR8, A, PCl, PCh); break; // XOR A, n + case 0xEF: RST_(0x28); break; // RST 0x28 + case 0xF0: RET_COND(!FlagS); break; // RET p + case 0xF1: POP_(F, A); break; // POP AF + case 0xF2: JP_COND(!FlagS); break; // JP p + case 0xF3: DI_(); break; // DI + case 0xF4: CALL_COND(!FlagS); break; // CALL p + case 0xF5: PUSH_(F, A); break; // PUSH AF + case 0xF6: REG_OP_IND_INC(OR8, A, PCl, PCh); break; // OR A, n + case 0xF7: RST_(0x30); break; // RST 0x30 + case 0xF8: RET_COND(FlagS); break; // RET M + case 0xF9: LD_SP_16(Iyl, Iyh); break; // LD SP, Iy + case 0xFA: JP_COND(FlagS); break; // JP M + case 0xFB: EI_(); break; // EI + case 0xFC: CALL_COND(FlagS); break; // CALL M + case 0xFD: PREFIX_(IYpre); break; // IY Prefix + case 0xFE: REG_OP_IND_INC(CP8, A, PCl, PCh); break; // CP A, n + case 0xFF: RST_(0x38); break; // RST $38 + } + } + else if (IXCB_prefix || IYCB_prefix) + { + // the first byte fetched is the prefetch value to use with the instruction + // we pick Ix or Iy here, the indexed value is stored in WZ + // In this way, we don't need to pass them as an argument to the I_Funcs. + IXCB_prefix = false; + IYCB_prefix = false; + NO_prefix = true; + + switch (opcode) + { + case 0x00: I_INT_OP(RLC, B); break; // RLC (I* + n) -> B + case 0x01: I_INT_OP(RLC, C); break; // RLC (I* + n) -> C + case 0x02: I_INT_OP(RLC, D); break; // RLC (I* + n) -> D + case 0x03: I_INT_OP(RLC, E); break; // RLC (I* + n) -> E + case 0x04: I_INT_OP(RLC, H); break; // RLC (I* + n) -> H + case 0x05: I_INT_OP(RLC, L); break; // RLC (I* + n) -> L + case 0x06: I_INT_OP(RLC, ALU); break; // RLC (I* + n) + case 0x07: I_INT_OP(RLC, A); break; // RLC (I* + n) -> A + case 0x08: I_INT_OP(RRC, B); break; // RRC (I* + n) -> B + case 0x09: I_INT_OP(RRC, C); break; // RRC (I* + n) -> C + case 0x0A: I_INT_OP(RRC, D); break; // RRC (I* + n) -> D + case 0x0B: I_INT_OP(RRC, E); break; // RRC (I* + n) -> E + case 0x0C: I_INT_OP(RRC, H); break; // RRC (I* + n) -> H + case 0x0D: I_INT_OP(RRC, L); break; // RRC (I* + n) -> L + case 0x0E: I_INT_OP(RRC, ALU); break; // RRC (I* + n) + case 0x0F: I_INT_OP(RRC, A); break; // RRC (I* + n) -> A + case 0x10: I_INT_OP(RL, B); break; // RL (I* + n) -> B + case 0x11: I_INT_OP(RL, C); break; // RL (I* + n) -> C + case 0x12: I_INT_OP(RL, D); break; // RL (I* + n) -> D + case 0x13: I_INT_OP(RL, E); break; // RL (I* + n) -> E + case 0x14: I_INT_OP(RL, H); break; // RL (I* + n) -> H + case 0x15: I_INT_OP(RL, L); break; // RL (I* + n) -> L + case 0x16: I_INT_OP(RL, ALU); break; // RL (I* + n) + case 0x17: I_INT_OP(RL, A); break; // RL (I* + n) -> A + case 0x18: I_INT_OP(RR, B); break; // RR (I* + n) -> B + case 0x19: I_INT_OP(RR, C); break; // RR (I* + n) -> C + case 0x1A: I_INT_OP(RR, D); break; // RR (I* + n) -> D + case 0x1B: I_INT_OP(RR, E); break; // RR (I* + n) -> E + case 0x1C: I_INT_OP(RR, H); break; // RR (I* + n) -> H + case 0x1D: I_INT_OP(RR, L); break; // RR (I* + n) -> L + case 0x1E: I_INT_OP(RR, ALU); break; // RR (I* + n) + case 0x1F: I_INT_OP(RR, A); break; // RR (I* + n) -> A + case 0x20: I_INT_OP(SLA, B); break; // SLA (I* + n) -> B + case 0x21: I_INT_OP(SLA, C); break; // SLA (I* + n) -> C + case 0x22: I_INT_OP(SLA, D); break; // SLA (I* + n) -> D + case 0x23: I_INT_OP(SLA, E); break; // SLA (I* + n) -> E + case 0x24: I_INT_OP(SLA, H); break; // SLA (I* + n) -> H + case 0x25: I_INT_OP(SLA, L); break; // SLA (I* + n) -> L + case 0x26: I_INT_OP(SLA, ALU); break; // SLA (I* + n) + case 0x27: I_INT_OP(SLA, A); break; // SLA (I* + n) -> A + case 0x28: I_INT_OP(SRA, B); break; // SRA (I* + n) -> B + case 0x29: I_INT_OP(SRA, C); break; // SRA (I* + n) -> C + case 0x2A: I_INT_OP(SRA, D); break; // SRA (I* + n) -> D + case 0x2B: I_INT_OP(SRA, E); break; // SRA (I* + n) -> E + case 0x2C: I_INT_OP(SRA, H); break; // SRA (I* + n) -> H + case 0x2D: I_INT_OP(SRA, L); break; // SRA (I* + n) -> L + case 0x2E: I_INT_OP(SRA, ALU); break; // SRA (I* + n) + case 0x2F: I_INT_OP(SRA, A); break; // SRA (I* + n) -> A + case 0x30: I_INT_OP(SLL, B); break; // SLL (I* + n) -> B + case 0x31: I_INT_OP(SLL, C); break; // SLL (I* + n) -> C + case 0x32: I_INT_OP(SLL, D); break; // SLL (I* + n) -> D + case 0x33: I_INT_OP(SLL, E); break; // SLL (I* + n) -> E + case 0x34: I_INT_OP(SLL, H); break; // SLL (I* + n) -> H + case 0x35: I_INT_OP(SLL, L); break; // SLL (I* + n) -> L + case 0x36: I_INT_OP(SLL, ALU); break; // SLL (I* + n) + case 0x37: I_INT_OP(SLL, A); break; // SLL (I* + n) -> A + case 0x38: I_INT_OP(SRL, B); break; // SRL (I* + n) -> B + case 0x39: I_INT_OP(SRL, C); break; // SRL (I* + n) -> C + case 0x3A: I_INT_OP(SRL, D); break; // SRL (I* + n) -> D + case 0x3B: I_INT_OP(SRL, E); break; // SRL (I* + n) -> E + case 0x3C: I_INT_OP(SRL, H); break; // SRL (I* + n) -> H + case 0x3D: I_INT_OP(SRL, L); break; // SRL (I* + n) -> L + case 0x3E: I_INT_OP(SRL, ALU); break; // SRL (I* + n) + case 0x3F: I_INT_OP(SRL, A); break; // SRL (I* + n) -> A + case 0x40: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x41: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x42: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x43: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x44: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x45: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x46: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x47: I_BIT_TE(0); break; // BIT 0, (I* + n) + case 0x48: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x49: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x4A: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x4B: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x4C: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x4D: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x4E: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x4F: I_BIT_TE(1); break; // BIT 1, (I* + n) + case 0x50: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x51: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x52: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x53: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x54: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x55: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x56: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x57: I_BIT_TE(2); break; // BIT 2, (I* + n) + case 0x58: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x59: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x5A: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x5B: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x5C: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x5D: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x5E: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x5F: I_BIT_TE(3); break; // BIT 3, (I* + n) + case 0x60: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x61: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x62: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x63: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x64: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x65: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x66: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x67: I_BIT_TE(4); break; // BIT 4, (I* + n) + case 0x68: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x69: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x6A: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x6B: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x6C: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x6D: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x6E: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x6F: I_BIT_TE(5); break; // BIT 5, (I* + n) + case 0x70: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x71: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x72: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x73: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x74: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x75: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x76: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x77: I_BIT_TE(6); break; // BIT 6, (I* + n) + case 0x78: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x79: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x7A: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x7B: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x7C: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x7D: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x7E: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x7F: I_BIT_TE(7); break; // BIT 7, (I* + n) + case 0x80: I_BIT_OP(RES, 0, B); break; // RES 0, (I* + n) -> B + case 0x81: I_BIT_OP(RES, 0, C); break; // RES 0, (I* + n) -> C + case 0x82: I_BIT_OP(RES, 0, D); break; // RES 0, (I* + n) -> D + case 0x83: I_BIT_OP(RES, 0, E); break; // RES 0, (I* + n) -> E + case 0x84: I_BIT_OP(RES, 0, H); break; // RES 0, (I* + n) -> H + case 0x85: I_BIT_OP(RES, 0, L); break; // RES 0, (I* + n) -> L + case 0x86: I_BIT_OP(RES, 0, ALU); break; // RES 0, (I* + n) + case 0x87: I_BIT_OP(RES, 0, A); break; // RES 0, (I* + n) -> A + case 0x88: I_BIT_OP(RES, 1, B); break; // RES 1, (I* + n) -> B + case 0x89: I_BIT_OP(RES, 1, C); break; // RES 1, (I* + n) -> C + case 0x8A: I_BIT_OP(RES, 1, D); break; // RES 1, (I* + n) -> D + case 0x8B: I_BIT_OP(RES, 1, E); break; // RES 1, (I* + n) -> E + case 0x8C: I_BIT_OP(RES, 1, H); break; // RES 1, (I* + n) -> H + case 0x8D: I_BIT_OP(RES, 1, L); break; // RES 1, (I* + n) -> L + case 0x8E: I_BIT_OP(RES, 1, ALU); break; // RES 1, (I* + n) + case 0x8F: I_BIT_OP(RES, 1, A); break; // RES 1, (I* + n) -> A + case 0x90: I_BIT_OP(RES, 2, B); break; // RES 2, (I* + n) -> B + case 0x91: I_BIT_OP(RES, 2, C); break; // RES 2, (I* + n) -> C + case 0x92: I_BIT_OP(RES, 2, D); break; // RES 2, (I* + n) -> D + case 0x93: I_BIT_OP(RES, 2, E); break; // RES 2, (I* + n) -> E + case 0x94: I_BIT_OP(RES, 2, H); break; // RES 2, (I* + n) -> H + case 0x95: I_BIT_OP(RES, 2, L); break; // RES 2, (I* + n) -> L + case 0x96: I_BIT_OP(RES, 2, ALU); break; // RES 2, (I* + n) + case 0x97: I_BIT_OP(RES, 2, A); break; // RES 2, (I* + n) -> A + case 0x98: I_BIT_OP(RES, 3, B); break; // RES 3, (I* + n) -> B + case 0x99: I_BIT_OP(RES, 3, C); break; // RES 3, (I* + n) -> C + case 0x9A: I_BIT_OP(RES, 3, D); break; // RES 3, (I* + n) -> D + case 0x9B: I_BIT_OP(RES, 3, E); break; // RES 3, (I* + n) -> E + case 0x9C: I_BIT_OP(RES, 3, H); break; // RES 3, (I* + n) -> H + case 0x9D: I_BIT_OP(RES, 3, L); break; // RES 3, (I* + n) -> L + case 0x9E: I_BIT_OP(RES, 3, ALU); break; // RES 3, (I* + n) + case 0x9F: I_BIT_OP(RES, 3, A); break; // RES 3, (I* + n) -> A + case 0xA0: I_BIT_OP(RES, 4, B); break; // RES 4, (I* + n) -> B + case 0xA1: I_BIT_OP(RES, 4, C); break; // RES 4, (I* + n) -> C + case 0xA2: I_BIT_OP(RES, 4, D); break; // RES 4, (I* + n) -> D + case 0xA3: I_BIT_OP(RES, 4, E); break; // RES 4, (I* + n) -> E + case 0xA4: I_BIT_OP(RES, 4, H); break; // RES 4, (I* + n) -> H + case 0xA5: I_BIT_OP(RES, 4, L); break; // RES 4, (I* + n) -> L + case 0xA6: I_BIT_OP(RES, 4, ALU); break; // RES 4, (I* + n) + case 0xA7: I_BIT_OP(RES, 4, A); break; // RES 4, (I* + n) -> A + case 0xA8: I_BIT_OP(RES, 5, B); break; // RES 5, (I* + n) -> B + case 0xA9: I_BIT_OP(RES, 5, C); break; // RES 5, (I* + n) -> C + case 0xAA: I_BIT_OP(RES, 5, D); break; // RES 5, (I* + n) -> D + case 0xAB: I_BIT_OP(RES, 5, E); break; // RES 5, (I* + n) -> E + case 0xAC: I_BIT_OP(RES, 5, H); break; // RES 5, (I* + n) -> H + case 0xAD: I_BIT_OP(RES, 5, L); break; // RES 5, (I* + n) -> L + case 0xAE: I_BIT_OP(RES, 5, ALU); break; // RES 5, (I* + n) + case 0xAF: I_BIT_OP(RES, 5, A); break; // RES 5, (I* + n) -> A + case 0xB0: I_BIT_OP(RES, 6, B); break; // RES 6, (I* + n) -> B + case 0xB1: I_BIT_OP(RES, 6, C); break; // RES 6, (I* + n) -> C + case 0xB2: I_BIT_OP(RES, 6, D); break; // RES 6, (I* + n) -> D + case 0xB3: I_BIT_OP(RES, 6, E); break; // RES 6, (I* + n) -> E + case 0xB4: I_BIT_OP(RES, 6, H); break; // RES 6, (I* + n) -> H + case 0xB5: I_BIT_OP(RES, 6, L); break; // RES 6, (I* + n) -> L + case 0xB6: I_BIT_OP(RES, 6, ALU); break; // RES 6, (I* + n) + case 0xB7: I_BIT_OP(RES, 6, A); break; // RES 6, (I* + n) -> A + case 0xB8: I_BIT_OP(RES, 7, B); break; // RES 7, (I* + n) -> B + case 0xB9: I_BIT_OP(RES, 7, C); break; // RES 7, (I* + n) -> C + case 0xBA: I_BIT_OP(RES, 7, D); break; // RES 7, (I* + n) -> D + case 0xBB: I_BIT_OP(RES, 7, E); break; // RES 7, (I* + n) -> E + case 0xBC: I_BIT_OP(RES, 7, H); break; // RES 7, (I* + n) -> H + case 0xBD: I_BIT_OP(RES, 7, L); break; // RES 7, (I* + n) -> L + case 0xBE: I_BIT_OP(RES, 7, ALU); break; // RES 7, (I* + n) + case 0xBF: I_BIT_OP(RES, 7, A); break; // RES 7, (I* + n) -> A + case 0xC0: I_BIT_OP(SET, 0, B); break; // SET 0, (I* + n) -> B + case 0xC1: I_BIT_OP(SET, 0, C); break; // SET 0, (I* + n) -> C + case 0xC2: I_BIT_OP(SET, 0, D); break; // SET 0, (I* + n) -> D + case 0xC3: I_BIT_OP(SET, 0, E); break; // SET 0, (I* + n) -> E + case 0xC4: I_BIT_OP(SET, 0, H); break; // SET 0, (I* + n) -> H + case 0xC5: I_BIT_OP(SET, 0, L); break; // SET 0, (I* + n) -> L + case 0xC6: I_BIT_OP(SET, 0, ALU); break; // SET 0, (I* + n) + case 0xC7: I_BIT_OP(SET, 0, A); break; // SET 0, (I* + n) -> A + case 0xC8: I_BIT_OP(SET, 1, B); break; // SET 1, (I* + n) -> B + case 0xC9: I_BIT_OP(SET, 1, C); break; // SET 1, (I* + n) -> C + case 0xCA: I_BIT_OP(SET, 1, D); break; // SET 1, (I* + n) -> D + case 0xCB: I_BIT_OP(SET, 1, E); break; // SET 1, (I* + n) -> E + case 0xCC: I_BIT_OP(SET, 1, H); break; // SET 1, (I* + n) -> H + case 0xCD: I_BIT_OP(SET, 1, L); break; // SET 1, (I* + n) -> L + case 0xCE: I_BIT_OP(SET, 1, ALU); break; // SET 1, (I* + n) + case 0xCF: I_BIT_OP(SET, 1, A); break; // SET 1, (I* + n) -> A + case 0xD0: I_BIT_OP(SET, 2, B); break; // SET 2, (I* + n) -> B + case 0xD1: I_BIT_OP(SET, 2, C); break; // SET 2, (I* + n) -> C + case 0xD2: I_BIT_OP(SET, 2, D); break; // SET 2, (I* + n) -> D + case 0xD3: I_BIT_OP(SET, 2, E); break; // SET 2, (I* + n) -> E + case 0xD4: I_BIT_OP(SET, 2, H); break; // SET 2, (I* + n) -> H + case 0xD5: I_BIT_OP(SET, 2, L); break; // SET 2, (I* + n) -> L + case 0xD6: I_BIT_OP(SET, 2, ALU); break; // SET 2, (I* + n) + case 0xD7: I_BIT_OP(SET, 2, A); break; // SET 2, (I* + n) -> A + case 0xD8: I_BIT_OP(SET, 3, B); break; // SET 3, (I* + n) -> B + case 0xD9: I_BIT_OP(SET, 3, C); break; // SET 3, (I* + n) -> C + case 0xDA: I_BIT_OP(SET, 3, D); break; // SET 3, (I* + n) -> D + case 0xDB: I_BIT_OP(SET, 3, E); break; // SET 3, (I* + n) -> E + case 0xDC: I_BIT_OP(SET, 3, H); break; // SET 3, (I* + n) -> H + case 0xDD: I_BIT_OP(SET, 3, L); break; // SET 3, (I* + n) -> L + case 0xDE: I_BIT_OP(SET, 3, ALU); break; // SET 3, (I* + n) + case 0xDF: I_BIT_OP(SET, 3, A); break; // SET 3, (I* + n) -> A + case 0xE0: I_BIT_OP(SET, 4, B); break; // SET 4, (I* + n) -> B + case 0xE1: I_BIT_OP(SET, 4, C); break; // SET 4, (I* + n) -> C + case 0xE2: I_BIT_OP(SET, 4, D); break; // SET 4, (I* + n) -> D + case 0xE3: I_BIT_OP(SET, 4, E); break; // SET 4, (I* + n) -> E + case 0xE4: I_BIT_OP(SET, 4, H); break; // SET 4, (I* + n) -> H + case 0xE5: I_BIT_OP(SET, 4, L); break; // SET 4, (I* + n) -> L + case 0xE6: I_BIT_OP(SET, 4, ALU); break; // SET 4, (I* + n) + case 0xE7: I_BIT_OP(SET, 4, A); break; // SET 4, (I* + n) -> A + case 0xE8: I_BIT_OP(SET, 5, B); break; // SET 5, (I* + n) -> B + case 0xE9: I_BIT_OP(SET, 5, C); break; // SET 5, (I* + n) -> C + case 0xEA: I_BIT_OP(SET, 5, D); break; // SET 5, (I* + n) -> D + case 0xEB: I_BIT_OP(SET, 5, E); break; // SET 5, (I* + n) -> E + case 0xEC: I_BIT_OP(SET, 5, H); break; // SET 5, (I* + n) -> H + case 0xED: I_BIT_OP(SET, 5, L); break; // SET 5, (I* + n) -> L + case 0xEE: I_BIT_OP(SET, 5, ALU); break; // SET 5, (I* + n) + case 0xEF: I_BIT_OP(SET, 5, A); break; // SET 5, (I* + n) -> A + case 0xF0: I_BIT_OP(SET, 6, B); break; // SET 6, (I* + n) -> B + case 0xF1: I_BIT_OP(SET, 6, C); break; // SET 6, (I* + n) -> C + case 0xF2: I_BIT_OP(SET, 6, D); break; // SET 6, (I* + n) -> D + case 0xF3: I_BIT_OP(SET, 6, E); break; // SET 6, (I* + n) -> E + case 0xF4: I_BIT_OP(SET, 6, H); break; // SET 6, (I* + n) -> H + case 0xF5: I_BIT_OP(SET, 6, L); break; // SET 6, (I* + n) -> L + case 0xF6: I_BIT_OP(SET, 6, ALU); break; // SET 6, (I* + n) + case 0xF7: I_BIT_OP(SET, 6, A); break; // SET 6, (I* + n) -> A + case 0xF8: I_BIT_OP(SET, 7, B); break; // SET 7, (I* + n) -> B + case 0xF9: I_BIT_OP(SET, 7, C); break; // SET 7, (I* + n) -> C + case 0xFA: I_BIT_OP(SET, 7, D); break; // SET 7, (I* + n) -> D + case 0xFB: I_BIT_OP(SET, 7, E); break; // SET 7, (I* + n) -> E + case 0xFC: I_BIT_OP(SET, 7, H); break; // SET 7, (I* + n) -> H + case 0xFD: I_BIT_OP(SET, 7, L); break; // SET 7, (I* + n) -> L + case 0xFE: I_BIT_OP(SET, 7, ALU); break; // SET 7, (I* + n) + case 0xFF: I_BIT_OP(SET, 7, A); break; // SET 7, (I* + n) -> A + } + } + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Interrupts.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Interrupts.cs new file mode 100644 index 00000000000..99309dd69ff --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Interrupts.cs @@ -0,0 +1,142 @@ +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + public partial class Z80AOpt + { + private bool iff1; + public bool IFF1 + { + get => iff1; + set => iff1 = value; + } + + private bool iff2; + public bool IFF2 + { + get => iff2; + set => iff2 = value; + } + + private bool nonMaskableInterrupt; + public bool NonMaskableInterrupt + { + get => nonMaskableInterrupt; + set { if (value && !nonMaskableInterrupt) nonMaskableInterruptPending = true; nonMaskableInterrupt = value; } + } + + private bool nonMaskableInterruptPending; + + private int interruptMode; + public int InterruptMode + { + get => interruptMode; + set + { + if (value is < 0 or > 2) throw new ArgumentOutOfRangeException(paramName: nameof(value), value, message: "invalid interrupt mode"); + interruptMode = value; + } + } + + private void NMI_() + { + PopulateCURINSTR + (IDLE, + IDLE, + IDLE, + IDLE, + DEC16, SPl, SPh, + TR, ALU, PCl, + WAIT, + WR_DEC, SPl, SPh, PCh, + TR16, PCl, PCh, NMI_V, ZERO, + WAIT, + WR, SPl, SPh, ALU); + + PopulateBUSRQ(0, 0, 0, 0, 0, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, 0, 0, 0, 0, SPh, 0, 0, SPh, 0, 0); + IRQS = 11; + } + + // Mode 0 interrupts only take effect if a CALL or RST is on the data bus + // Otherwise operation just continues as normal + // For now assume a NOP is on the data bus, in which case no stack operations occur + + //NOTE: TODO: When a CALL is present on the data bus, adjust WZ accordingly + private void INTERRUPT_0(ushort src) + { + PopulateCURINSTR + (IDLE, + IDLE, + IORQ, + WAIT, + IDLE, + WAIT, + RD_INC, ALU, PCl, PCh); + + PopulateBUSRQ(0, 0, 0, 0, PCh, 0, 0); + PopulateMEMRQ(0, 0, 0, 0, PCh, 0, 0); + IRQS = 7; + } + + // Just jump to $0038 + private void INTERRUPT_1() + { + PopulateCURINSTR + (IDLE, + IDLE, + IORQ, + WAIT, + IDLE, + TR, ALU, PCl, + DEC16, SPl, SPh, + IDLE, + WAIT, + WR_DEC, SPl, SPh, PCh, + TR16, PCl, PCh, IRQ_V, ZERO, + WAIT, + WR, SPl, SPh, ALU); + + PopulateBUSRQ(0, 0, 0, 0, I, 0, 0, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, 0, 0, 0, I, 0, 0, SPh, 0, 0, SPh, 0, 0); + IRQS = 13; + } + + // Interrupt mode 2 uses the I vector combined with a byte on the data bus + private void INTERRUPT_2() + { + PopulateCURINSTR + (IDLE, + IDLE, + IORQ, + WAIT, + FTCH_DB, + IDLE, + DEC16, SPl, SPh, + TR16, Z, W, DB, I, + WAIT, + WR_DEC, SPl, SPh, PCh, + IDLE, + WAIT, + WR, SPl, SPh, PCl, + IDLE, + WAIT, + RD_INC, PCl, Z, W, + IDLE, + WAIT, + RD, PCh, Z, W); + + PopulateBUSRQ(0, 0, 0, 0, I, 0, 0, SPh, 0, 0, SPh, 0, 0, W, 0, 0, W, 0, 0); + PopulateMEMRQ(0, 0, 0, 0, I, 0, 0, SPh, 0, 0, SPh, 0, 0, W, 0, 0, W, 0, 0); + IRQS = 19; + } + + private void ResetInterrupts() + { + IFF1 = false; + IFF2 = false; + NonMaskableInterrupt = false; + nonMaskableInterruptPending = false; + FlagI = false; + InterruptMode = 1; + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Operations.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Operations.cs new file mode 100644 index 00000000000..9dc7c7c8fff --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Operations.cs @@ -0,0 +1,755 @@ +using BizHawk.Common.NumberExtensions; + +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + public partial class Z80AOpt + { + public void Read_Func(ushort dest, ushort src_l, ushort src_h) + { + Regs[dest] = _link.ReadMemory((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[DB] = Regs[dest]; + } + + public void Read_INC_Func(ushort dest, ushort src_l, ushort src_h) + { + Regs[dest] = _link.ReadMemory((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[DB] = Regs[dest]; + INC16_Func(src_l, src_h); + } + + public void Read_INC_TR_PC_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + Regs[dest_h] = _link.ReadMemory((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[DB] = Regs[dest_h]; + INC16_Func(src_l, src_h); + TR16_Func(PCl, PCh, dest_l, dest_h); + } + + public void Write_Func(ushort dest_l, ushort dest_h, ushort src) + { + Regs[DB] = Regs[src]; + _link.WriteMemory((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)Regs[src]); + } + + public void Write_INC_Func(ushort dest_l, ushort dest_h, ushort src) + { + Regs[DB] = Regs[src]; + _link.WriteMemory((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)Regs[src]); + INC16_Func(dest_l, dest_h); + } + + public void Write_DEC_Func(ushort dest_l, ushort dest_h, ushort src) + { + Regs[DB] = Regs[src]; + _link.WriteMemory((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)Regs[src]); + DEC16_Func(dest_l, dest_h); + } + + public void Write_TR_PC_Func(ushort dest_l, ushort dest_h, ushort src) + { + Regs[DB] = Regs[src]; + _link.WriteMemory((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)Regs[src]); + TR16_Func(PCl, PCh, Z, W); + } + + public void OUT_Func(ushort dest_l, ushort dest_h, ushort src) + { + Regs[DB] = Regs[src]; + _link.WriteHardware((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)(Regs[src])); + } + + public void OUT_INC_Func(ushort dest_l, ushort dest_h, ushort src) + { + Regs[DB] = Regs[src]; + _link.WriteHardware((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)(Regs[src])); + INC16_Func(dest_l, dest_h); + } + + public void IN_Func(ushort dest, ushort src_l, ushort src_h) + { + // Flags accumulated into F once (see ADD8_Func). IN preserves C. + byte val = _link.ReadHardware((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[dest] = val; + Regs[DB] = val; + + byte f = (byte)(Regs[F] & 0x01); // preserve C + f |= (byte)(val & 0xA8); // S, F5, F3 + if (val == 0) f |= 0x40; // Z + if (TableParity[val]) f |= 0x04; // P + // H = N = 0 + Regs[F] = f; + } + + public void IN_INC_Func(ushort dest, ushort src_l, ushort src_h) + { + byte val = _link.ReadHardware((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[dest] = val; + Regs[DB] = val; + + byte f = (byte)(Regs[F] & 0x01); // preserve C + f |= (byte)(val & 0xA8); // S, F5, F3 + if (val == 0) f |= 0x40; // Z + if (TableParity[val]) f |= 0x04; // P + // H = N = 0 + Regs[F] = f; + + INC16_Func(src_l, src_h); + } + + public void IN_A_N_INC_Func(ushort dest, ushort src_l, ushort src_h) + { + Regs[dest] = _link.ReadHardware((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[DB] = Regs[dest]; + INC16_Func(src_l, src_h); + } + + public void TR_Func(ushort dest, ushort src) + { + Regs[dest] = Regs[src]; + } + + public void TR16_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + Regs[dest_l] = Regs[src_l]; + Regs[dest_h] = Regs[src_h]; + } + + public void ADD16_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + int Reg16_d = Regs[dest_l] | (Regs[dest_h] << 8); + int Reg16_s = Regs[src_l] | (Regs[src_h] << 8); + int temp = Reg16_d + Reg16_s; + + // ADD16 preserves S, Z, P; accumulate the rest into F once. + byte f = (byte)(Regs[F] & 0xC4); // preserve S, Z, P + if (temp.Bit(16)) f |= 0x01; // C + if (((Reg16_d & 0xFFF) + (Reg16_s & 0xFFF)) > 0xFFF) f |= 0x10; // H + if ((temp & 0x0800) != 0) f |= 0x08; // F3 + if ((temp & 0x2000) != 0) f |= 0x20; // F5 + // N = 0 + Regs[dest_l] = (ushort)(temp & 0xFF); + Regs[dest_h] = (ushort)((temp & 0xFF00) >> 8); + Regs[F] = f; + } + + public void ADD8_Func(ushort dest, ushort src) + { + // Flags are accumulated into a single local and written to F once, instead of the + // 8 read-modify-writes of Regs[F] the flag properties would do. Bit masks: + // C=0x01 N=0x02 P=0x04 F3=0x08 H=0x10 F5=0x20 Z=0x40 S=0x80. Values are identical. + int d = Regs[dest]; + int s = Regs[src]; + int sum = d + s; + ushort ans = (ushort)(sum & 0xFF); + + byte f = (byte)(ans & 0x28); // F3, F5 from result + if ((sum & 0x100) != 0) f |= 0x01; // C + if ((((d & 0xF) + (s & 0xF)) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (((d ^ s) & 0x80) == 0 && ((d ^ ans) & 0x80) != 0) f |= 0x04; // P/V (overflow) + // N = 0 + + Regs[dest] = ans; + Regs[F] = f; + } + + public void SUB8_Func(ushort dest, ushort src) + { + int d = Regs[dest]; + int s = Regs[src]; + int diff = d - s; + ushort ans = (ushort)(diff & 0xFF); + + byte f = (byte)((ans & 0x28) | 0x02); // F3, F5, N + if ((diff & 0x100) != 0) f |= 0x01; // C (borrow) + if ((((d & 0xF) - (s & 0xF)) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (((d ^ s) & 0x80) != 0 && ((d ^ ans) & 0x80) != 0) f |= 0x04; // P/V (overflow) + + Regs[dest] = ans; + Regs[F] = f; + } + + public void BIT_Func(ushort bit, ushort src) + { + FlagZ = !Regs[src].Bit(bit); + FlagP = FlagZ; // special case + FlagH = true; + FlagN = false; + FlagS = ((bit == 7) && Regs[src].Bit(bit)); + Flag5 = Regs[src].Bit(5); + Flag3 = Regs[src].Bit(3); + } + + // When doing I* + n bit tests, flags 3 and 5 come from I* + n + // This cooresponds to the high byte of WZ + // This is the same for the (HL) bit tests, except that WZ were not assigned to before the test occurs + public void I_BIT_Func(ushort bit, ushort src) + { + FlagZ = !Regs[src].Bit(bit); + FlagP = FlagZ; // special case + FlagH = true; + FlagN = false; + FlagS = ((bit == 7) && Regs[src].Bit(bit)); + Flag5 = Regs[W].Bit(5); + Flag3 = Regs[W].Bit(3); + } + + public void SET_Func(ushort bit, ushort src) + { + Regs[src] |= (ushort)(1 << bit); + } + + public void RES_Func(ushort bit, ushort src) + { + Regs[src] &= (ushort)(0xFF - (1 << bit)); + } + + public void ASGN_Func(ushort src, ushort val) + { + Regs[src] = val; + } + + public void SLL_Func(ushort src) + { + int v = Regs[src]; + byte f = (byte)((v >> 7) & 0x01); // C = old bit 7 + int r = ((v << 1) & 0xFF) | 0x01; + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void SLA_Func(ushort src) + { + int v = Regs[src]; + byte f = (byte)((v >> 7) & 0x01); // C = old bit 7 + int r = (v << 1) & 0xFF; + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void SRA_Func(ushort src) + { + int v = Regs[src]; + byte f = (byte)(v & 0x01); // C = old bit 0 + int r = (v >> 1) | (v & 0x80); // MSB unchanged + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void SRL_Func(ushort src) + { + int v = Regs[src]; + byte f = (byte)(v & 0x01); // C = old bit 0 + int r = v >> 1; + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void CPL_Func(ushort src) + { + Regs[src] = (ushort)((~Regs[src]) & 0xFF); + + FlagH = true; + FlagN = true; + Flag3 = (Regs[src] & 0x08) != 0; + Flag5 = (Regs[src] & 0x20) != 0; + } + + public void CCF_Func(ushort src) + { + FlagH = FlagC; + FlagC = !FlagC; + FlagN = false; + Flag3 = (Regs[src] & 0x08) != 0; + Flag5 = (Regs[src] & 0x20) != 0; + } + + public void SCF_Func(ushort src) + { + FlagC = true; + FlagH = false; + FlagN = false; + Flag3 = (Regs[src] & 0x08) != 0; + Flag5 = (Regs[src] & 0x20) != 0; + } + + public void AND8_Func(ushort dest, ushort src) + { + ushort ans = (ushort)(Regs[dest] & Regs[src]); + + byte f = (byte)((ans & 0x28) | 0x10); // F3, F5, H (C=N=0) + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (TableParity[ans]) f |= 0x04; // P + + Regs[dest] = ans; + Regs[F] = f; + } + + public void OR8_Func(ushort dest, ushort src) + { + ushort ans = (ushort)(Regs[dest] | Regs[src]); + + byte f = (byte)(ans & 0x28); // F3, F5 (C=H=N=0) + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (TableParity[ans]) f |= 0x04; // P + + Regs[dest] = ans; + Regs[F] = f; + } + + public void XOR8_Func(ushort dest, ushort src) + { + ushort ans = (ushort)(Regs[dest] ^ Regs[src]); + + byte f = (byte)(ans & 0x28); // F3, F5 (C=H=N=0) + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (TableParity[ans]) f |= 0x04; // P + + Regs[dest] = ans; + Regs[F] = f; + } + + public void CP8_Func(ushort dest, ushort src) + { + // CP is SUB with the result discarded; F3/F5 come from the SOURCE operand, not the result. + int d = Regs[dest]; + int s = Regs[src]; + int diff = d - s; + ushort ans = (ushort)(diff & 0xFF); + + byte f = (byte)((s & 0x28) | 0x02); // F3, F5 from src; N + if ((diff & 0x100) != 0) f |= 0x01; // C + if ((((d & 0xF) - (s & 0xF)) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (((d ^ s) & 0x80) != 0 && ((d ^ ans) & 0x80) != 0) f |= 0x04; // P/V + + Regs[F] = f; + } + + public void RRC_Func(ushort src) + { + bool imm = src == Aim; + if (imm) { src = A; } + + int v = Regs[src]; + byte f = (byte)(v & 0x01); // C = old bit 0 + int r = ((v & 0x01) << 7) | (v >> 1); + if (imm) + f |= (byte)((Regs[F] & 0xC4) | (r & 0x28)); // preserve S,Z,P; F3,F5 from result + else + { + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + } + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void RR_Func(ushort src) + { + bool imm = src == Aim; + if (imm) { src = A; } + + int fin = Regs[F]; + int v = Regs[src]; + byte f = (byte)(v & 0x01); // new C = old bit 0 + int r = ((fin & 0x01) << 7) | (v >> 1); // shift in old carry + if (imm) + f |= (byte)((fin & 0xC4) | (r & 0x28)); + else + { + f |= (byte)(r & 0xA8); + if (r == 0) f |= 0x40; + if (TableParity[r]) f |= 0x04; + } + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void RLC_Func(ushort src) + { + bool imm = src == Aim; + if (imm) { src = A; } + + int v = Regs[src]; + byte f = (byte)((v >> 7) & 0x01); // C = old bit 7 + int r = ((v << 1) & 0xFF) | ((v >> 7) & 0x01); + if (imm) + f |= (byte)((Regs[F] & 0xC4) | (r & 0x28)); + else + { + f |= (byte)(r & 0xA8); + if (r == 0) f |= 0x40; + if (TableParity[r]) f |= 0x04; + } + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void RL_Func(ushort src) + { + bool imm = src == Aim; + if (imm) { src = A; } + + int fin = Regs[F]; + int v = Regs[src]; + byte f = (byte)((v >> 7) & 0x01); // new C = old bit 7 + int r = ((v << 1) & 0xFF) | (fin & 0x01); // shift in old carry + if (imm) + f |= (byte)((fin & 0xC4) | (r & 0x28)); + else + { + f |= (byte)(r & 0xA8); + if (r == 0) f |= 0x40; + if (TableParity[r]) f |= 0x04; + } + // H = N = 0 + Regs[src] = (ushort)r; + Regs[F] = f; + } + + public void INC8_Func(ushort src) + { + // INC8 does NOT affect the carry flag — preserve it. + int v = Regs[src]; + ushort ans = (ushort)((v + 1) & 0xFF); + + byte f = (byte)(Regs[F] & 0x01); // preserve C + f |= (byte)(ans & 0x28); // F3, F5 + if ((((v & 0xF) + 1) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (ans == 0x80) f |= 0x04; // P/V (overflow into 0x80) + // N = 0 + + Regs[src] = ans; + Regs[F] = f; + } + + public void DEC8_Func(ushort src) + { + // DEC8 does NOT affect the carry flag — preserve it. + int v = Regs[src]; + ushort ans = (ushort)((v - 1) & 0xFF); + + byte f = (byte)((Regs[F] & 0x01) | 0x02); // preserve C; N + f |= (byte)(ans & 0x28); // F3, F5 + if ((((v & 0xF) - 1) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (ans == 0x7F) f |= 0x04; // P/V + + Regs[src] = ans; + Regs[F] = f; + } + + public void INC16_Func(ushort src_l, ushort src_h) + { + int Reg16_d = Regs[src_l] | (Regs[src_h] << 8); + + Reg16_d += 1; + + Regs[src_l] = (ushort)(Reg16_d & 0xFF); + Regs[src_h] = (ushort)((Reg16_d & 0xFF00) >> 8); + } + + public void DEC16_Func(ushort src_l, ushort src_h) + { + int Reg16_d = Regs[src_l] | (Regs[src_h] << 8); + + Reg16_d -= 1; + + Regs[src_l] = (ushort)(Reg16_d & 0xFF); + Regs[src_h] = (ushort)((Reg16_d & 0xFF00) >> 8); + } + + public void ADC8_Func(ushort dest, ushort src) + { + int d = Regs[dest]; + int s = Regs[src]; + int c = Regs[F] & 0x01; // carry-in (read before F is overwritten) + int sum = d + s + c; + ushort ans = (ushort)(sum & 0xFF); + + byte f = (byte)(ans & 0x28); + if ((sum & 0x100) != 0) f |= 0x01; // C + if ((((d & 0xF) + (s & 0xF) + c) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (((d ^ s) & 0x80) == 0 && ((d ^ ans) & 0x80) != 0) f |= 0x04; // P/V + // N = 0 + + Regs[dest] = ans; + Regs[F] = f; + } + + public void SBC8_Func(ushort dest, ushort src) + { + int d = Regs[dest]; + int s = Regs[src]; + int c = Regs[F] & 0x01; // carry-in + int diff = d - s - c; + ushort ans = (ushort)(diff & 0xFF); + + byte f = (byte)((ans & 0x28) | 0x02); // F3, F5, N + if ((diff & 0x100) != 0) f |= 0x01; // C + if ((((d & 0xF) - (s & 0xF) - c) & 0x10) != 0) f |= 0x10; // H + if (ans == 0) f |= 0x40; // Z + if ((ans & 0x80) != 0) f |= 0x80; // S + if (((d ^ s) & 0x80) != 0 && ((d ^ ans) & 0x80) != 0) f |= 0x04; // P/V + + Regs[dest] = ans; + Regs[F] = f; + } + + public void DA_Func(ushort src) + { + int fin = Regs[F]; + bool fN = (fin & 0x02) != 0; + bool fH = (fin & 0x10) != 0; + bool fC = (fin & 0x01) != 0; + + byte a = (byte)Regs[src]; + int temp = a; // int + final &0xFF matches byte wrap + + if (fN) + { + if (fH || ((a & 0x0F) > 0x09)) { temp -= 0x06; } + if (fC || a > 0x99) { temp -= 0x60; } + } + else + { + if (fH || ((a & 0x0F) > 0x09)) { temp += 0x06; } + if (fC || a > 0x99) { temp += 0x60; } + } + + temp &= 0xFF; + + // DAA preserves N; accumulate the rest into F once. + byte f = (byte)(fin & 0x02); // preserve N + if (fC || a > 0x99) f |= 0x01; // C + if (temp == 0) f |= 0x40; // Z + if (((a ^ temp) & 0x10) != 0) f |= 0x10; // H + if (TableParity[temp]) f |= 0x04; // P + f |= (byte)(temp & 0xA8); // S, F5, F3 + Regs[src] = (ushort)temp; + Regs[F] = f; + } + + // used for signed operations + public void ADDS_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + int Reg16_d = Regs[dest_l]; + int Reg16_s = Regs[src_l]; + + Reg16_d += Reg16_s; + + ushort temp = 0; + + // since this is signed addition, calculate the high byte carry appropriately + // note that flags are unaffected by this operation + if (Reg16_s.Bit(7)) + { + if (((Reg16_d & 0xFF) >= Regs[dest_l])) + { + temp = 0xFF; + } + else + { + temp = 0; + } + } + else + { + temp = (ushort)(Reg16_d.Bit(8) ? 1 : 0); + } + + ushort ans_l = (ushort)(Reg16_d & 0xFF); + + Regs[dest_l] = ans_l; + Regs[dest_h] += temp; + Regs[dest_h] &= 0xFF; + } + + public void EXCH_16_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + ushort temp = Regs[dest_l]; + Regs[dest_l] = Regs[src_l]; + Regs[src_l] = temp; + + temp = Regs[dest_h]; + Regs[dest_h] = Regs[src_h]; + Regs[src_h] = temp; + } + + public void SBC_16_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + int Reg16_d = Regs[dest_l] | (Regs[dest_h] << 8); + int Reg16_s = Regs[src_l] | (Regs[src_h] << 8); + int c = Regs[F] & 0x01; + + int ans = Reg16_d - Reg16_s - c; + + // half carry: same computation as before, on a copy of the low 12 bits + int hd = (Reg16_d & 0xFFF) - ((Reg16_s & 0xFFF) + c); + + byte f = 0x02; // N + if (ans.Bit(16)) f |= 0x01; // C + if ((Reg16_d.Bit(15) != Reg16_s.Bit(15)) && (Reg16_d.Bit(15) != ans.Bit(15))) f |= 0x04; // P/V + if ((ushort)(ans & 0xFFFF) > 32767) f |= 0x80; // S + if ((ans & 0xFFFF) == 0) f |= 0x40; // Z + if ((ans & 0x0800) != 0) f |= 0x08; // F3 + if ((ans & 0x2000) != 0) f |= 0x20; // F5 + if (hd.Bit(12)) f |= 0x10; // H + Regs[dest_l] = (ushort)(ans & 0xFF); + Regs[dest_h] = (ushort)((ans >> 8) & 0xFF); + Regs[F] = f; + } + + public void ADC_16_Func(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + int Reg16_d = Regs[dest_l] | (Regs[dest_h] << 8); + int Reg16_s = Regs[src_l] | (Regs[src_h] << 8); + + int c = Regs[F] & 0x01; + int ans = Reg16_d + Reg16_s + c; + + byte f = 0; // N = 0 + if (((Reg16_d & 0xFFF) + (Reg16_s & 0xFFF) + c) > 0xFFF) f |= 0x10; // H + if (ans.Bit(16)) f |= 0x01; // C + if ((Reg16_d.Bit(15) == Reg16_s.Bit(15)) && (Reg16_d.Bit(15) != ans.Bit(15))) f |= 0x04; // P/V + if ((ans & 0xFFFF) > 32767) f |= 0x80; // S + if ((ans & 0xFFFF) == 0) f |= 0x40; // Z + if ((ans & 0x0800) != 0) f |= 0x08; // F3 + if ((ans & 0x2000) != 0) f |= 0x20; // F5 + Regs[dest_l] = (ushort)(ans & 0xFF); + Regs[dest_h] = (ushort)((ans >> 8) & 0xFF); + Regs[F] = f; + } + + public void NEG_8_Func(ushort src) + { + int s = Regs[src]; + ushort ans = (ushort)((-s) & 0xFF); + + byte f = 0x02; // N + if (s != 0x00) f |= 0x01; // C + if (ans == 0) f |= 0x40; // Z + if (s == 0x80) f |= 0x04; // P/V + f |= (byte)(ans & 0xA8); // S, F5, F3 + if (((-(s & 0xF)) & 0x10) != 0) f |= 0x10; // H + Regs[src] = ans; + Regs[F] = f; + } + + public void RRD_Func(ushort dest, ushort src) + { + ushort temp1 = Regs[src]; + ushort temp2 = Regs[dest]; + Regs[dest] = (ushort)(((temp1 & 0x0F) << 4) + ((temp2 & 0xF0) >> 4)); + Regs[src] = (ushort)((temp1 & 0xF0) + (temp2 & 0x0F)); + + int r = Regs[src]; + byte f = (byte)(Regs[F] & 0x01); // preserve C + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + // H = N = 0 + Regs[F] = f; + } + + public void RLD_Func(ushort dest, ushort src) + { + ushort temp1 = Regs[src]; + ushort temp2 = Regs[dest]; + Regs[dest] = (ushort)((temp1 & 0x0F) + ((temp2 & 0x0F) << 4)); + Regs[src] = (ushort)((temp1 & 0xF0) + ((temp2 & 0xF0) >> 4)); + + int r = Regs[src]; + byte f = (byte)(Regs[F] & 0x01); // preserve C + f |= (byte)(r & 0xA8); // S, F5, F3 + if (r == 0) f |= 0x40; // Z + if (TableParity[r]) f |= 0x04; // P + // H = N = 0 + Regs[F] = f; + } + + // sets flags for LD/R + public void SET_FL_LD_Func() + { + FlagP = (Regs[C] | (Regs[B] << 8)) != 0; + FlagH = false; + FlagN = false; + Flag5 = ((Regs[ALU] + Regs[A]) & 0x02) != 0; + Flag3 = ((Regs[ALU] + Regs[A]) & 0x08) != 0; + } + + // set flags for CP/R + public void SET_FL_CP_Func() + { + int Reg8_d = Regs[A]; + int Reg8_s = Regs[ALU]; + + // get half carry flag + byte temp = (byte)((Reg8_d & 0xF) - (Reg8_s & 0xF)); + FlagH = temp.Bit(4); + + temp = (byte)(Reg8_d - Reg8_s); + FlagN = true; + FlagZ = temp == 0; + FlagS = temp > 127; + FlagP = (Regs[C] | (Regs[B] << 8)) != 0; + + temp = (byte)(Reg8_d - Reg8_s - (FlagH ? 1 : 0)); + Flag5 = (temp & 0x02) != 0; + Flag3 = (temp & 0x08) != 0; + } + + // set flags for LD A, I/R + public void SET_FL_IR_Func(ushort dest) + { + if (dest == A) + { + FlagN = false; + FlagH = false; + FlagZ = Regs[A] == 0; + FlagS = Regs[A] > 127; + FlagP = iff2; + Flag5 = (Regs[A] & 0x20) != 0; + Flag3 = (Regs[A] & 0x08) != 0; + } + } + + public void FTCH_DB_Func() + { + Regs[DB] = _link.FetchDB(); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Registers.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Registers.cs new file mode 100644 index 00000000000..dca494f58b9 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Registers.cs @@ -0,0 +1,151 @@ +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + public partial class Z80AOpt + { + // registers + public ushort PCl = 0; + public ushort PCh = 1; + public ushort SPl = 2; + public ushort SPh = 3; + public ushort A = 4; + public ushort F = 5; + public ushort B = 6; + public ushort C = 7; + public ushort D = 8; + public ushort E = 9; + public ushort H = 10; + public ushort L = 11; + public ushort W = 12; + public ushort Z = 13; + public ushort Aim = 14; // use this indicator for RLCA etc., since the Z flag is reset on those + public ushort Ixl = 15; + public ushort Ixh = 16; + public ushort Iyl = 17; + public ushort Iyh = 18; + public ushort Int = 19; + public ushort R = 20; + public ushort I = 21; + public ushort ZERO = 22; // it is convenient to have a register that is always zero, to reuse instructions + public ushort ALU = 23; // This will be temporary arthimatic storage + // shadow registers + public ushort A_s = 24; + public ushort F_s = 25; + public ushort B_s = 26; + public ushort C_s = 27; + public ushort D_s = 28; + public ushort E_s = 29; + public ushort H_s = 30; + public ushort L_s = 31; + public ushort DB = 32; + public ushort scratch = 33; + public ushort IRQ_V = 34; // IRQ mode 1 vector + public ushort NMI_V = 35; // NMI vector + + public ushort[] Regs = new ushort[36]; + + // IO Contention Constants. Need to distinguish port access and normal memory accesses for zx spectrum + public const ushort BIO1 = 100; + public const ushort BIO2 = 101; + public const ushort BIO3 = 102; + public const ushort BIO4 = 103; + + public const ushort WIO1 = 105; + public const ushort WIO2 = 106; + public const ushort WIO3 = 107; + public const ushort WIO4 = 108; + + + public bool FlagI; + + public bool FlagW; // wait flag, when set to true reads / writes will be delayed + + public bool FlagC + { + get => (Regs[5] & 0x01) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x01) | (value ? 0x01 : 0x00)); + } + + public bool FlagN + { + get => (Regs[5] & 0x02) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x02) | (value ? 0x02 : 0x00)); + } + + public bool FlagP + { + get => (Regs[5] & 0x04) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x04) | (value ? 0x04 : 0x00)); + } + + public bool Flag3 + { + get => (Regs[5] & 0x08) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x08) | (value ? 0x08 : 0x00)); + } + + public bool FlagH + { + get => (Regs[5] & 0x10) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x10) | (value ? 0x10 : 0x00)); + } + + public bool Flag5 + { + get => (Regs[5] & 0x20) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x20) | (value ? 0x20 : 0x00)); + } + + public bool FlagZ + { + get => (Regs[5] & 0x40) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x40) | (value ? 0x40 : 0x00)); + } + + public bool FlagS + { + get => (Regs[5] & 0x80) != 0; + set => Regs[5] = (ushort)((Regs[5] & ~0x80) | (value ? 0x80 : 0x00)); + } + + public ushort RegPC + { + get => (ushort)(Regs[0] | (Regs[1] << 8)); + set + { + Regs[0] = (ushort)(value & 0xFF); + Regs[1] = (ushort)((value >> 8) & 0xFF); + } + } + + private void ResetRegisters() + { + for (int i=0; i < 36; i++) + { + Regs[i] = 0; + } + + // the IRQ1 vector is 0x38 + Regs[IRQ_V] = 0x38; + // The NMI vector is constant 0x66 + Regs[NMI_V] = 0x66; + + FlagI = false; + FlagW = false; + } + + private bool[] TableParity; + private void InitTableParity() + { + TableParity = new bool[256]; + for (int i = 0; i < 256; ++i) + { + int Bits = 0; + for (int j = 0; j < 8; ++j) + { + Bits += (i >> j) & 1; + } + TableParity[i] = (Bits & 1) == 0; + } + } + } +} \ No newline at end of file diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Tables_Direct.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Tables_Direct.cs new file mode 100644 index 00000000000..73e2dafeb6a --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Tables_Direct.cs @@ -0,0 +1,639 @@ +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + public partial class Z80AOpt + { + // this contains the vectors of instrcution operations + // NOTE: This list is NOT confirmed accurate for each individual cycle + + private void NOP_() + { + PopulateCURINSTR + (IDLE); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + // NOTE: In a real Z80, this operation just flips a switch to choose between 2 registers + // but it's simpler to emulate just by exchanging the register with it's shadow + private void EXCH_() + { + PopulateCURINSTR + (EXCH); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void EXX_() + { + PopulateCURINSTR + (EXX); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + // this exchanges 2 16 bit registers + private void EXCH_16_(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (EXCH_16, dest_l, dest_h, src_l, src_h); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void INC_16(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (INC16, src_l, src_h, + IDLE, + IDLE); + + PopulateBUSRQ(0, I, I); + PopulateMEMRQ(0, 0, 0); + IRQS = 3; + } + + + private void DEC_16(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (DEC16, src_l, src_h, + IDLE, + IDLE); + + PopulateBUSRQ(0, I, I); + PopulateMEMRQ(0, 0, 0); + IRQS = 3; + } + + // this is done in two steps technically, but the flags don't work out using existing funcitons + // so let's use a different function since it's an internal operation anyway + private void ADD_16(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, dest_l, dest_h, + IDLE, + INC16, Z, W, + IDLE, + ADD16, dest_l, dest_h, src_l, src_h, + IDLE, + IDLE); + + PopulateBUSRQ(0, I, I, I, I, I, I, I); + PopulateMEMRQ(0, 0, 0, 0, 0, 0, 0, 0); + IRQS = 8; + } + + private void REG_OP(ushort operation, ushort dest, ushort src) + { + PopulateCURINSTR + (operation, dest, src); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + // Operations using the I and R registers take one T-cycle longer + private void REG_OP_IR(ushort operation, ushort dest, ushort src) + { + PopulateCURINSTR + (IDLE, + SET_FL_IR, dest, src); + + PopulateBUSRQ(0, I); + PopulateMEMRQ(0, 0); + IRQS = 2; + } + + // note: do not use DEC here since no flags are affected by this operation + private void DJNZ_() + { + if ((Regs[B] - 1) != 0) + { + PopulateCURINSTR + (IDLE, + IDLE, + ASGN, B, (ushort)((Regs[B] - 1) & 0xFF), + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + IDLE, + ASGN, W, 0, + ADDS, PCl, PCh, Z, W, + TR16, Z, W, PCl, PCh); + + PopulateBUSRQ(0, I, PCh, 0, 0, PCh, PCh, PCh, PCh, PCh); + PopulateMEMRQ(0, 0, PCh, 0, 0, 0, 0, 0, 0, 0); + IRQS = 10; + } + else + { + PopulateCURINSTR + (IDLE, + IDLE, + ASGN, B, (ushort)((Regs[B] - 1) & 0xFF), + WAIT, + RD_INC, ALU, PCl, PCh); + + PopulateBUSRQ(0, I, PCh, 0, 0); + PopulateMEMRQ(0, 0, PCh, 0, 0); + IRQS = 5; + } + } + + private void HALT_() + { + PopulateCURINSTR + (HALT); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void JR_COND(bool cond) + { + if (cond) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + ASGN, W, 0, + IDLE, + ADDS, PCl, PCh, Z, W, + TR16, Z, W, PCl, PCh); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, PCh, PCh, PCh, PCh); + PopulateMEMRQ(0, PCh, 0, 0, 0, 0, 0, 0, 0); + IRQS = 9; + } + else + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, ALU, PCl, PCh); + + PopulateBUSRQ(0, PCh, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0); + IRQS = 4; + } + } + + private void JP_COND(bool cond) + { + if (cond) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC_TR_PC, Z, W, PCl, PCh); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0); + IRQS = 7; + } + else + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC, W, PCl, PCh); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0); + IRQS = 7; + } + } + + private void RET_() + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, SPl, SPh, + IDLE, + WAIT, + RD_INC_TR_PC, Z, W, SPl, SPh); + + PopulateBUSRQ(0, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, SPh, 0, 0, SPh, 0, 0); + IRQS = 7; + } + + private void RETI_() + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, SPl, SPh, + IDLE, + WAIT, + RD_INC_TR_PC, Z, W, SPl, SPh); + + PopulateBUSRQ(0, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, SPh, 0, 0, SPh, 0, 0); + IRQS = 7; + } + + private void RETN_() + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, SPl, SPh, + EI_RETN, + WAIT, + RD_INC_TR_PC, Z, W, SPl, SPh); + + PopulateBUSRQ(0, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, SPh, 0, 0, SPh, 0, 0); + IRQS = 7; + } + + + private void RET_COND(bool cond) + { + if (cond) + { + PopulateCURINSTR + (IDLE, + IDLE, + IDLE, + WAIT, + RD_INC, Z, SPl, SPh, + IDLE, + WAIT, + RD_INC_TR_PC, Z, W, SPl, SPh); + + PopulateBUSRQ(0, I, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, 0, SPh, 0, 0, SPh, 0, 0); + IRQS = 8; + } + else + { + PopulateCURINSTR + (IDLE, + IDLE); + + PopulateBUSRQ(0, I); + PopulateMEMRQ(0, 0); + IRQS = 2; + } + } + + private void CALL_COND(bool cond) + { + if (cond) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD, W, PCl, PCh, + INC16, PCl, PCh, + DEC16, SPl, SPh, + WAIT, + WR_DEC, SPl, SPh, PCh, + IDLE, + WAIT, + WR_TR_PC, SPl, SPh, PCl); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, PCh, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, 0, SPh, 0, 0, SPh, 0, 0); + IRQS = 14; + } + else + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC, W, PCl, PCh); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0); + IRQS = 7; + } + } + + private void INT_OP(ushort operation, ushort src) + { + PopulateCURINSTR + (operation, src); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void BIT_OP(ushort operation, ushort bit, ushort src) + { + PopulateCURINSTR + (operation, bit, src); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void PUSH_(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + DEC16, SPl, SPh, + IDLE, + WAIT, + WR_DEC, SPl, SPh, src_h, + IDLE, + WAIT, + WR, SPl, SPh, src_l); + + PopulateBUSRQ(0, I, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, 0, SPh, 0, 0, SPh, 0, 0); + IRQS = 8; + } + + + private void POP_(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, src_l, SPl, SPh, + IDLE, + WAIT, + RD_INC, src_h, SPl, SPh); + + PopulateBUSRQ(0, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, SPh, 0, 0, SPh, 0, 0); + IRQS = 7; + } + + private void RST_(ushort n) + { + PopulateCURINSTR + (IDLE, + DEC16, SPl, SPh, + IDLE, + WAIT, + WR_DEC, SPl, SPh, PCh, + RST, n, + WAIT, + WR_TR_PC, SPl, SPh, PCl); + + PopulateBUSRQ(0, I, SPh, 0, 0, SPh, 0, 0); + PopulateMEMRQ(0, 0, SPh, 0, 0, SPh, 0, 0); + IRQS = 8; + } + + private void PREFIX_(ushort src) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + PREFIX); + + PRE_SRC = src; + + PopulateBUSRQ(0, PCh, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0); + IRQS = -1; // prefix does not get interrupted + } + + private void PREFETCH_(ushort src) + { + if (src == IXCBpre) + { + Regs[W] = Regs[Ixh]; + Regs[Z] = Regs[Ixl]; + } + else + { + Regs[W] = Regs[Iyh]; + Regs[Z] = Regs[Iyl]; + } + + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, ALU, PCl, PCh, + ADDS, Z, W, ALU, ZERO, + WAIT, + IDLE, + PREFIX); + + PRE_SRC = src; + + //Console.WriteLine(TotalExecutedCycles); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, PCh); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, 0); + IRQS = -1; // prefetch does not get interrupted + } + + private void DI_() + { + PopulateCURINSTR + (DI); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void EI_() + { + PopulateCURINSTR + (EI); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void JP_16(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (TR16, PCl, PCh, src_l, src_h); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void LD_SP_16(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + TR16, SPl, SPh, src_l, src_h); + + PopulateBUSRQ(0, I, I); + PopulateMEMRQ(0, 0, 0); + IRQS = 3; + } + + private void OUT_() + { + PopulateCURINSTR + (IDLE, + TR, W, A, + WAIT, + RD_INC, Z, PCl, PCh, + TR, ALU, A, + IDLE, + WAIT, + OUT_INC, Z, ALU, A); + + PopulateBUSRQ(0, PCh, 0, 0, WIO1, WIO2, WIO3, WIO4); + PopulateMEMRQ(0, PCh, 0, 0, WIO1, WIO2, WIO3, WIO4); + IRQS = 8; + } + + private void OUT_REG_(ushort dest, ushort src) + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, C, B, + IDLE, + WAIT, + OUT_INC, Z, W, src); + + PopulateBUSRQ(0, BIO1, BIO2, BIO3, BIO4); + PopulateMEMRQ(0, BIO1, BIO2, BIO3, BIO4); + IRQS = 5; + } + + private void IN_() + { + PopulateCURINSTR + (IDLE, + TR, W, A, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + IDLE, + WAIT, + IN_A_N_INC, A, Z, W); + + PopulateBUSRQ(0, PCh, 0, 0, WIO1, WIO2, WIO3, WIO4); + PopulateMEMRQ(0, PCh, 0, 0, WIO1, WIO2, WIO3, WIO4); + IRQS = 8; + } + + private void IN_REG_(ushort dest, ushort src) + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, C, B, + IDLE, + WAIT, + IN_INC, dest, Z, W); + + PopulateBUSRQ(0, BIO1, BIO2, BIO3, BIO4); + PopulateMEMRQ(0, BIO1, BIO2, BIO3, BIO4); + IRQS = 5; + } + + private void REG_OP_16_(ushort op, ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + IDLE, + TR16, Z, W, dest_l, dest_h, + INC16, Z, W, + IDLE, + IDLE, + op, dest_l, dest_h, src_l, src_h); + + PopulateBUSRQ(0, I, I, I, I, I, I, I); + PopulateMEMRQ(0, 0, 0, 0, 0, 0, 0, 0); + IRQS = 8; + } + + private void INT_MODE_(ushort src) + { + PopulateCURINSTR + (INT_MODE, src); + + PopulateBUSRQ(0); + PopulateMEMRQ(0); + IRQS = 1; + } + + private void RRD_() + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, L, H, + WAIT, + RD, ALU, Z, W, + IDLE, + RRD, ALU, A, + IDLE, + IDLE, + IDLE, + WAIT, + WR_INC, Z, W, ALU); + + PopulateBUSRQ(0, H, 0, 0, H, H, H, H, W, 0, 0); + PopulateMEMRQ(0, H, 0, 0, 0, 0, 0, 0, W, 0, 0); + IRQS = 11; + } + + private void RLD_() + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, L, H, + WAIT, + RD, ALU, Z, W, + IDLE, + RLD, ALU, A, + IDLE, + IDLE, + IDLE, + WAIT, + WR_INC, Z, W, ALU); + + PopulateBUSRQ(0, H, 0, 0, H, H, H, H, W, 0, 0); + PopulateMEMRQ(0, H, 0, 0, 0, 0, 0, 0, W, 0, 0); + IRQS = 11; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Tables_Indirect.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Tables_Indirect.cs new file mode 100644 index 00000000000..2ea91290183 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Tables_Indirect.cs @@ -0,0 +1,520 @@ +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + public partial class Z80AOpt + { + private void INT_OP_IND(ushort operation, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, src_l, src_h, + IDLE, + operation, ALU, + WAIT, + WR, src_l, src_h, ALU); + + PopulateBUSRQ(0, src_h, 0, 0, src_h, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0, 0, src_h, 0, 0); + IRQS = 8; + } + + private void BIT_OP_IND(ushort operation, ushort bit, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, src_l, src_h, + operation, bit, ALU, + IDLE, + WAIT, + WR, src_l, src_h, ALU); + + PopulateBUSRQ(0, src_h, 0, 0, src_h, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0, 0, src_h, 0, 0); + IRQS = 8; + } + + // Note that this operation uses I_BIT, same as indexed BIT. + // This is where the strange behaviour in Flag bits 3 and 5 come from. + // normally WZ contain I* + n when doing I_BIT ops, but here we use that code path + // even though WZ is not assigned to, letting it's value from other operations show through + private void BIT_TE_IND(ushort operation, ushort bit, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, src_l, src_h, + I_BIT, bit, ALU); + + PopulateBUSRQ(0, src_h, 0, 0, src_h); + PopulateMEMRQ(0, src_h, 0, 0, 0); + IRQS = 5; + } + + private void REG_OP_IND_INC(ushort operation, ushort dest, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_OP, 1, ALU, src_l, src_h, operation, dest, ALU); + + PopulateBUSRQ(0, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0); + IRQS = 4; + } + + private void REG_OP_IND(ushort operation, ushort dest, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, src_l, src_h, + WAIT, + RD_OP, 1, ALU, Z, W, operation, dest, ALU); + + PopulateBUSRQ(0, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0); + IRQS = 4; + } + + // different because HL doesn't effect WZ + private void REG_OP_IND_HL(ushort operation, ushort dest) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_OP, 0, ALU, L, H, operation, dest, ALU); + + PopulateBUSRQ(0, H, 0, 0); + PopulateMEMRQ(0, H, 0, 0); + IRQS = 4; + } + + private void LD_16_IND_nn(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC, W, PCl, PCh, + IDLE, + WAIT, + WR_INC, Z, W, src_l, + IDLE, + WAIT, + WR, Z, W, src_h); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0, W, 0, 0); + IRQS = 13; + } + + private void LD_IND_16_nn(ushort dest_l, ushort dest_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC, W, PCl, PCh, + IDLE, + WAIT, + RD_INC, dest_l, Z, W, + IDLE, + WAIT, + RD, dest_h, Z, W); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0, W, 0, 0); + IRQS = 13; + } + + private void LD_8_IND_nn(ushort src) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC, W, PCl, PCh, + IDLE, + WAIT, + WR_INC_WA, Z, W, src); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0); + IRQS = 10; + } + + private void LD_IND_8_nn(ushort dest) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, PCl, PCh, + IDLE, + WAIT, + RD_INC, W, PCl, PCh, + IDLE, + WAIT, + RD_INC, dest, Z, W); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, W, 0, 0); + IRQS = 10; + } + + private void LD_8_IND(ushort dest_l, ushort dest_h, ushort src) + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, dest_l, dest_h, + WAIT, + WR_INC_WA, Z, W, src); + + PopulateBUSRQ(0, dest_h, 0, 0); + PopulateMEMRQ(0, dest_h, 0, 0); + IRQS = 4; + } + + // seperate HL needed since it doesn't effect the WZ pair + private void LD_8_IND_HL(ushort src) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + WR, L, H, src); + + PopulateBUSRQ(0, H, 0, 0); + PopulateMEMRQ(0, H, 0, 0); + IRQS = 4; + } + + private void LD_8_IND_IND(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, ALU, src_l, src_h, + IDLE, + WAIT, + WR, dest_l, dest_h, ALU); + + PopulateBUSRQ(0, src_h, 0, 0, dest_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0, dest_h, 0, 0); + IRQS = 7; + } + + private void LD_IND_8_INC(ushort dest, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, dest, src_l, src_h); + + PopulateBUSRQ(0, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0); + IRQS = 4; + } + + private void LD_IND_16(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, dest_l, src_l, src_h, + IDLE, + WAIT, + RD_INC, dest_h, src_l, src_h); + + PopulateBUSRQ(0, src_h, 0, 0, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0, src_h, 0, 0); + IRQS = 7; + } + + private void INC_8_IND(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, src_l, src_h, + INC8, ALU, + IDLE, + WAIT, + WR, src_l, src_h, ALU); + + PopulateBUSRQ(0, src_h, 0, 0, src_h, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0, 0, src_h, 0, 0); + IRQS = 8; + } + + private void DEC_8_IND(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, src_l, src_h, + DEC8, ALU, + IDLE, + WAIT, + WR, src_l, src_h, ALU); + + PopulateBUSRQ(0, src_h, 0, 0, src_h, src_h, 0, 0); + PopulateMEMRQ(0, src_h, 0, 0, 0, src_h, 0, 0); + IRQS = 8; + } + + // NOTE: WZ implied for the wollowing 3 functions + private void I_INT_OP(ushort operation, ushort dest) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, Z, W, + operation, ALU, + TR, dest, ALU, + WAIT, + WR, Z, W, ALU); + + PopulateBUSRQ(0, W, 0, 0, W, W, 0, 0); + PopulateMEMRQ(0, W, 0, 0, 0, W, 0, 0); + IRQS = 8; + } + + private void I_BIT_OP(ushort operation, ushort bit, ushort dest) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, Z, W, + operation, bit, ALU, + TR, dest, ALU, + WAIT, + WR, Z, W, ALU); + + PopulateBUSRQ(0, W, 0, 0, W, W, 0, 0); + PopulateMEMRQ(0, W, 0, 0, 0, W, 0, 0); + IRQS = 8; + } + + private void I_BIT_TE(ushort bit) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, Z, W, + I_BIT, bit, ALU); + + PopulateBUSRQ(0, W, 0, 0, W); + PopulateMEMRQ(0, W, 0, 0, 0); + IRQS = 5; + } + + private void I_OP_n(ushort operation, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, PCl, PCh, + IDLE, + IDLE, + TR16, Z, W, src_l, src_h, + ADDS, Z, W, ALU, ZERO, + IDLE, + INC16, PCl, PCh, + WAIT, + RD, ALU, Z, W, + operation, ALU, + IDLE, + WAIT, + WR, Z, W, ALU); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, PCh, PCh, PCh, PCh, W, 0, 0, W, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, 0, 0, 0, 0, 0, W, 0, 0, 0, W, 0, 0); + IRQS = 16; + } + + private void I_OP_n_n(ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + TR16, Z, W, src_l, src_h, + WAIT, + RD_INC, ALU, PCl, PCh, + ADDS, Z, W, ALU, ZERO, + WAIT, + RD, ALU, PCl, PCh, + IDLE, + IDLE, + INC16, PCl, PCh, + WAIT, + WR, Z, W, ALU); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, 0, 0, PCh, PCh, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, PCh, 0, 0, 0, 0, W, 0, 0); + IRQS = 12; + } + + private void I_REG_OP_IND_n(ushort operation, ushort dest, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, PCl, PCh, + IDLE, + TR16, Z, W, src_l, src_h, + IDLE, + ADDS, Z, W, ALU, ZERO, + IDLE, + INC16, PCl, PCh, + WAIT, + RD_OP, 0, ALU, Z, W, operation, dest, ALU); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, PCh, PCh, PCh, PCh, W, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, 0, 0, 0, 0, 0, W, 0, 0); + IRQS = 12; + } + + private void I_LD_8_IND_n(ushort dest_l, ushort dest_h, ushort src) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, PCl, PCh, + IDLE, + IDLE, + TR16, Z, W, dest_l, dest_h, + ADDS, Z, W, ALU, ZERO, + IDLE, + INC16, PCl, PCh, + WAIT, + WR, Z, W, src); + + PopulateBUSRQ(0, PCh, 0, 0, PCh, PCh, PCh, PCh, PCh, Z, 0, 0); + PopulateMEMRQ(0, PCh, 0, 0, 0, 0, 0, 0, 0, Z, 0, 0); + IRQS = 12; + } + + private void LD_OP_R(ushort operation, ushort repeat_instr) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, L, H, + operation, L, H, + WAIT, + WR, E, D, ALU, + IDLE, + SET_FL_LD_R, 0, operation, repeat_instr); + + PopulateBUSRQ(0, H, 0, 0, D, 0, 0, D, D); + PopulateMEMRQ(0, H, 0, 0, D, 0, 0, 0, 0); + IRQS = 9; + } + + private void CP_OP_R(ushort operation, ushort repeat_instr) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD, ALU, L, H, + IDLE, + DEC16, C, B, + operation, Z, W, + IDLE, + SET_FL_CP_R, 1, operation, repeat_instr); + + PopulateBUSRQ(0, H, 0, 0, H, H, H, H, H); + PopulateMEMRQ(0, H, 0, 0, 0, 0, 0, 0, 0); + IRQS = 9; + } + + private void IN_OP_R(ushort operation, ushort repeat_instr) + { + PopulateCURINSTR + (IDLE, + IDLE, + IDLE, + IDLE, + WAIT, + IN, ALU, C, B, + IDLE, + WAIT, + REP_OP_I, L, H, ALU, operation, 2, operation, repeat_instr); + + PopulateBUSRQ(0, I, BIO1, BIO2, BIO3, BIO4, H, 0, 0); + PopulateMEMRQ(0, 0, BIO1, BIO2, BIO3, BIO4, H, 0, 0); + IRQS = 9; + } + + private void OUT_OP_R(ushort operation, ushort repeat_instr) + { + PopulateCURINSTR + (IDLE, + IDLE, + IDLE, + WAIT, + RD, ALU, L, H, + IDLE, + IDLE, + WAIT, + REP_OP_O, C, B, ALU, operation, 3, operation, repeat_instr); + + PopulateBUSRQ(0, I, H, 0, 0, BIO1, BIO2, BIO3, BIO4); + PopulateMEMRQ(0, 0, H, 0, 0, BIO1, BIO2, BIO3, BIO4); + IRQS = 9; + } + + // this is an indirect change of a a 16 bit register with memory + private void EXCH_16_IND_(ushort dest_l, ushort dest_h, ushort src_l, ushort src_h) + { + PopulateCURINSTR + (IDLE, + IDLE, + WAIT, + RD_INC, Z, dest_l, dest_h, + IDLE, + WAIT, + RD, W, dest_l, dest_h, + IDLE, + IDLE, + WAIT, + WR_DEC, dest_l, dest_h, src_h, + IDLE, + WAIT, + WR, dest_l, dest_h, src_l, + IDLE, + TR16, src_l, src_h, Z, W); + + PopulateBUSRQ(0, dest_h, 0, 0, dest_h, 0, 0, dest_h, dest_h, 0, 0, dest_h, 0, 0, dest_h, dest_h); + PopulateMEMRQ(0, dest_h, 0, 0, dest_h, 0, 0, 0, dest_h, 0, 0, dest_h, 0, 0, 0, 0); + IRQS = 16; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Z80A.cs b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Z80A.cs new file mode 100644 index 00000000000..ce96ad91192 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/CPUs/Z80AOpt/Z80A.cs @@ -0,0 +1,955 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Common.NumberExtensions; + +// Z80A CPU +using BizHawk.Emulation.Cores.Components.Z80A; + +namespace BizHawk.Emulation.Cores.Components.Z80AOpt +{ + /// + /// this type parameter might look useless—and it is—but after monomorphisation, + /// this way happens to perform better than the alternative + /// + /// + public sealed partial class Z80AOpt where TLink : IZ80ALink + { + // operations that can take place in an instruction + public const ushort IDLE = 0; + public const ushort OP = 1; + public const ushort OP_F = 2; // used for repeating operations + public const ushort HALT = 3; + public const ushort RD = 4; + public const ushort WR = 5; + public const ushort RD_INC = 6; // read and increment + public const ushort WR_INC = 7; // write and increment + public const ushort WR_DEC = 8; // write and increment (for stack pointer) + public const ushort TR = 9; + public const ushort TR16 = 10; + public const ushort ADD16 = 11; + public const ushort ADD8 = 12; + public const ushort SUB8 = 13; + public const ushort ADC8 = 14; + public const ushort SBC8 = 15; + public const ushort SBC16 = 16; + public const ushort ADC16 = 17; + public const ushort INC16 = 18; + public const ushort INC8 = 19; + public const ushort DEC16 = 20; + public const ushort DEC8 = 21; + public const ushort RLC = 22; + public const ushort RL = 23; + public const ushort RRC = 24; + public const ushort RR = 25; + public const ushort CPL = 26; + public const ushort DA = 27; + public const ushort SCF = 28; + public const ushort CCF = 29; + public const ushort AND8 = 30; + public const ushort XOR8 = 31; + public const ushort OR8 = 32; + public const ushort CP8 = 33; + public const ushort SLA = 34; + public const ushort SRA = 35; + public const ushort SRL = 36; + public const ushort SLL = 37; + public const ushort BIT = 38; + public const ushort RES = 39; + public const ushort SET = 40; + public const ushort EI = 41; + public const ushort DI = 42; + public const ushort EXCH = 43; + public const ushort EXX = 44; + public const ushort EXCH_16 = 45; + public const ushort PREFIX = 46; + public const ushort PREFETCH = 47; + public const ushort ASGN = 48; + public const ushort ADDS = 49; // signed 16 bit operation used in 2 instructions + public const ushort INT_MODE = 50; + public const ushort EI_RETN = 51; + public const ushort EI_RETI = 52; // reti has no delay in interrupt enable + public const ushort OUT = 53; + public const ushort IN = 54; + public const ushort NEG = 55; + public const ushort RRD = 56; + public const ushort RLD = 57; + public const ushort SET_FL_LD_R = 58; + public const ushort SET_FL_CP_R = 59; + public const ushort SET_FL_IR = 60; + public const ushort I_BIT = 61; + public const ushort HL_BIT = 62; + public const ushort FTCH_DB = 63; + public const ushort WAIT = 64; // enterred when reading or writing and FlagW is true + public const ushort RST = 65; + public const ushort REP_OP_I = 66; + public const ushort REP_OP_O = 67; + public const ushort IN_A_N_INC = 68; + public const ushort RD_INC_TR_PC = 69; // transfer WZ to PC after read + public const ushort WR_TR_PC = 70; // transfer WZ to PC after write + public const ushort OUT_INC = 71; + public const ushort IN_INC = 72; + public const ushort WR_INC_WA = 73; // A -> W after WR_INC + public const ushort RD_OP = 74; + public const ushort IORQ = 75; + + // non-state variables + public ushort Ztemp1, Ztemp2, Ztemp3, Ztemp4; + public byte temp_R; + + private TLink _link; + + public Z80AOpt(TLink link) + { + _link = link; + Reset(); + InitTableParity(); + } + + public void Reset() + { + ResetRegisters(); + ResetInterrupts(); + TotalExecutedCycles = 0; + + PopulateCURINSTR + (IDLE, + DEC16, F, A, + DEC16, SPl, SPh); + + PopulateBUSRQ(0, 0, 0); + PopulateMEMRQ(0, 0, 0); + IRQS = 3; + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + NO_prefix = true; + } + + public IDictionary GetCpuFlagsAndRegisters() + { + return new Dictionary + { + ["A"] = Regs[A], + ["AF"] = Regs[F] + (Regs[A] << 8), + ["B"] = Regs[B], + ["BC"] = Regs[C] + (Regs[B] << 8), + ["C"] = Regs[C], + ["D"] = Regs[D], + ["DE"] = Regs[E] + (Regs[D] << 8), + ["E"] = Regs[E], + ["F"] = Regs[F], + ["H"] = Regs[H], + ["HL"] = Regs[L] + (Regs[H] << 8), + ["I"] = Regs[I], + ["IX"] = Regs[Ixl] + (Regs[Ixh] << 8), + ["IY"] = Regs[Iyl] + (Regs[Iyh] << 8), + ["L"] = Regs[L], + ["PC"] = Regs[PCl] + (Regs[PCh] << 8), + ["R"] = Regs[R], + ["Shadow AF"] = Regs[F_s] + (Regs[A_s] << 8), + ["Shadow BC"] = Regs[C_s] + (Regs[B_s] << 8), + ["Shadow DE"] = Regs[E_s] + (Regs[D_s] << 8), + ["Shadow HL"] = Regs[L_s] + (Regs[H_s] << 8), + ["SP"] = Regs[Iyl] + (Regs[Iyh] << 8), + ["Flag C"] = FlagC, + ["Flag N"] = FlagN, + ["Flag P/V"] = FlagP, + ["Flag 3rd"] = Flag3, + ["Flag H"] = FlagH, + ["Flag 5th"] = Flag5, + ["Flag Z"] = FlagZ, + ["Flag S"] = FlagS + }; + } + + public void SetCpuRegister(string register, int value) + { + switch (register) + { + default: + throw new InvalidOperationException(); + case "A": + Regs[A] = (ushort)value; + break; + case "AF": + Regs[F] = (ushort)(value & 0xFF); + Regs[A] = (ushort)(value & 0xFF00); + break; + case "B": + Regs[B] = (ushort)value; + break; + case "BC": + Regs[C] = (ushort)(value & 0xFF); + Regs[B] = (ushort)(value & 0xFF00); + break; + case "C": + Regs[C] = (ushort)value; + break; + case "D": + Regs[D] = (ushort)value; + break; + case "DE": + Regs[E] = (ushort)(value & 0xFF); + Regs[D] = (ushort)(value & 0xFF00); + break; + case "E": + Regs[E] = (ushort)value; + break; + case "F": + Regs[F] = (ushort)value; + break; + case "H": + Regs[H] = (ushort)value; + break; + case "HL": + Regs[L] = (ushort)(value & 0xFF); + Regs[H] = (ushort)(value & 0xFF00); + break; + case "I": + Regs[I] = (ushort)value; + break; + case "IX": + Regs[Ixl] = (ushort)(value & 0xFF); + Regs[Ixh] = (ushort)(value & 0xFF00); + break; + case "IY": + Regs[Iyl] = (ushort)(value & 0xFF); + Regs[Iyh] = (ushort)(value & 0xFF00); + break; + case "L": + Regs[L] = (ushort)value; + break; + case "PC": + Regs[PCl] = (ushort)(value & 0xFF); + Regs[PCh] = (ushort)(value & 0xFF00); + break; + case "R": + Regs[R] = (ushort)value; + break; + case "Shadow AF": + Regs[F_s] = (ushort)(value & 0xFF); + Regs[A_s] = (ushort)(value & 0xFF00); + break; + case "Shadow BC": + Regs[C_s] = (ushort)(value & 0xFF); + Regs[B_s] = (ushort)(value & 0xFF00); + break; + case "Shadow DE": + Regs[E_s] = (ushort)(value & 0xFF); + Regs[D_s] = (ushort)(value & 0xFF00); + break; + case "Shadow HL": + Regs[L_s] = (ushort)(value & 0xFF); + Regs[H_s] = (ushort)(value & 0xFF00); + break; + case "SP": + Regs[SPl] = (ushort)(value & 0xFF); + Regs[SPh] = (ushort)(value & 0xFF00); + break; + } + } + + public void SetCpuLink(TLink link) + => _link = link; + + // Execute instructions + public void ExecuteOne() + { + // Bounds-check elimination for the per-cycle micro-op reads: take one ref to element 0 + // (a single bounds check) and index off it with Unsafe.Add (no per-access check). The + // ref is re-derived every call, so it stays valid across savestate array replacement. + // Indices are bounded by the instruction builders exactly as before, so this is safe. + ref ushort ci0 = ref cur_instr[0]; + bus_pntr++; mem_pntr++; + switch (Unsafe.Add(ref ci0, instr_pntr++)) + { + case IDLE: + // do nothing + break; + case OP: + // should never reach here + + break; + case OP_F: + // Read the opcode of the next instruction + _link.OnExecFetch(RegPC); + TraceCallback?.Invoke(State()); + opcode = _link.FetchMemory(RegPC++); + FetchInstruction(); + + temp_R = (byte)(Regs[R] & 0x7F); + temp_R++; + temp_R &= 0x7F; + Regs[R] = (byte)((Regs[R] & 0x80) | temp_R); + + instr_pntr = bus_pntr = mem_pntr = irq_pntr = 0; + I_skip = true; + break; + case HALT: + halted = true; + // NOTE: Check how halt state effects the DB + Regs[DB] = 0xFF; + + temp_R = (byte)(Regs[R] & 0x7F); + temp_R++; + temp_R &= 0x7F; + Regs[R] = (byte)((Regs[R] & 0x80) | temp_R); + break; + case RD: + Read_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case WR: + Write_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RD_INC: + Read_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RD_INC_TR_PC: + Read_INC_TR_PC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RD_OP: + if (Unsafe.Add(ref ci0, instr_pntr++) == 1) { Read_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); } + else { Read_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); } + + switch (Unsafe.Add(ref ci0, instr_pntr++)) + { + case ADD8: + ADD8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case ADC8: + ADC8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SUB8: + SUB8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SBC8: + SBC8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case AND8: + AND8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case XOR8: + XOR8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case OR8: + OR8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case CP8: + CP8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case TR: + TR_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + } + break; + case WR_INC: + Write_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case WR_DEC: + Write_DEC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case WR_TR_PC: + Write_TR_PC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case WR_INC_WA: + Write_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + Regs[W] = Regs[A]; + break; + case TR: + TR_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case TR16: + TR16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case ADD16: + ADD16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case ADD8: + ADD8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SUB8: + SUB8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case ADC8: + ADC8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case ADC16: + ADC_16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SBC8: + SBC8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SBC16: + SBC_16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case INC16: + INC16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case INC8: + INC8_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case DEC16: + DEC16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case DEC8: + DEC8_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RLC: + RLC_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RL: + RL_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RRC: + RRC_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RR: + RR_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case CPL: + CPL_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case DA: + DA_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SCF: + SCF_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case CCF: + CCF_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case AND8: + AND8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case XOR8: + XOR8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case OR8: + OR8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case CP8: + CP8_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SLA: + SLA_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SRA: + SRA_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SRL: + SRL_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SLL: + SLL_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case BIT: + BIT_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case I_BIT: + I_BIT_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RES: + RES_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SET: + SET_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case EI: + EI_pending = 2; + break; + case DI: + IFF1 = IFF2 = false; + break; + case EXCH: + EXCH_16_Func(F_s, A_s, F, A); + break; + case EXX: + EXCH_16_Func(C_s, B_s, C, B); + EXCH_16_Func(E_s, D_s, E, D); + EXCH_16_Func(L_s, H_s, L, H); + break; + case EXCH_16: + EXCH_16_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case PREFIX: + ushort src_t = PRE_SRC; + + NO_prefix = false; + if (PRE_SRC == CBpre) { CB_prefix = true; } + if (PRE_SRC == EXTDpre) { EXTD_prefix = true; } + if (PRE_SRC == IXpre) { IX_prefix = true; } + if (PRE_SRC == IYpre) { IY_prefix = true; } + if (PRE_SRC == IXCBpre) { IXCB_prefix = true; } + if (PRE_SRC == IYCBpre) { IYCB_prefix = true; } + + // only the first prefix in a double prefix increases R, although I don't know how / why + if (PRE_SRC < 4) + { + temp_R = (byte)(Regs[R] & 0x7F); + temp_R++; + temp_R &= 0x7F; + Regs[R] = (byte)((Regs[R] & 0x80) | temp_R); + } + + opcode = _link.FetchMemory(RegPC++); + FetchInstruction(); + instr_pntr = bus_pntr = mem_pntr = irq_pntr = 0; + I_skip = true; + + // for prefetched case, the PC stays on the BUS one cycle longer + if ((src_t == IXCBpre) || (src_t == IYCBpre)) { BUSRQ[0] = PCh; } + + break; + case ASGN: + ASGN_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case ADDS: + ADDS_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case EI_RETI: + // NOTE: This is needed for systems using multiple interrupt sources, it triggers the next interrupt + // Not currently implemented here + iff1 = iff2; + break; + case EI_RETN: + iff1 = iff2; + break; + case OUT: + OUT_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case OUT_INC: + OUT_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case IN: + IN_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case IN_INC: + IN_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case IN_A_N_INC: + IN_A_N_INC_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case NEG: + NEG_8_Func(Unsafe.Add(ref ci0, instr_pntr++)); + break; + case INT_MODE: + interruptMode = Unsafe.Add(ref ci0, instr_pntr++); + break; + case RRD: + RRD_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case RLD: + RLD_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + break; + case SET_FL_LD_R: + DEC16_Func(C, B); + SET_FL_LD_Func(); + + Ztemp1 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp2 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp3 = Unsafe.Add(ref ci0, instr_pntr++); + + if (((Regs[C] | (Regs[B] << 8)) != 0) && (Ztemp3 > 0)) + { + PopulateCURINSTR + (DEC16, PCl, PCh, + DEC16, PCl, PCh, + TR16, Z, W, PCl, PCh, + INC16, Z, W, + Ztemp2, E, D); + + PopulateBUSRQ(D, D, D, D, D); + PopulateMEMRQ(0, 0, 0, 0, 0); + IRQS = 5; + + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + I_skip = true; + } + else + { + if (Ztemp2 == INC16) { INC16_Func(E, D); } + else { DEC16_Func(E, D); } + } + break; + case SET_FL_CP_R: + SET_FL_CP_Func(); + + Ztemp1 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp2 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp3 = Unsafe.Add(ref ci0, instr_pntr++); + + if (((Regs[C] | (Regs[B] << 8)) != 0) && (Ztemp3 > 0) && !FlagZ) + { + + PopulateCURINSTR + (DEC16, PCl, PCh, + DEC16, PCl, PCh, + TR16, Z, W, PCl, PCh, + INC16, Z, W, + Ztemp2, L, H); + + PopulateBUSRQ(H, H, H, H, H); + PopulateMEMRQ(0, 0, 0, 0, 0); + IRQS = 5; + + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + I_skip = true; + } + else + { + if (Ztemp2 == INC16) { INC16_Func(L, H); } + else { DEC16_Func(L, H); } + } + break; + case SET_FL_IR: + ushort dest_t = Unsafe.Add(ref ci0, instr_pntr++); + TR_Func(dest_t, Unsafe.Add(ref ci0, instr_pntr++)); + SET_FL_IR_Func(dest_t); + break; + case FTCH_DB: + FTCH_DB_Func(); + break; + case WAIT: + if (FlagW) + { + instr_pntr--; bus_pntr--; mem_pntr--; + I_skip = true; + } + break; + case RST: + Regs[Z] = Unsafe.Add(ref ci0, instr_pntr++); + Regs[W] = 0; + break; + case REP_OP_I: + Write_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + + Ztemp4 = Unsafe.Add(ref ci0, instr_pntr++); + if (Ztemp4 == DEC16) + { + TR16_Func(Z, W, C, B); + DEC16_Func(Z, W); + DEC8_Func(B); + + // take care of other flags + // taken from 'undocumented z80 documented' and Fuse + FlagN = Regs[ALU].Bit(7); + FlagH = FlagC = ((Regs[ALU] + Regs[C] - 1) & 0xFF) < Regs[ALU]; + FlagP = TableParity[((Regs[ALU] + Regs[C] - 1) & 7) ^ Regs[B]]; + } + else + { + TR16_Func(Z, W, C, B); + INC16_Func(Z, W); + DEC8_Func(B); + + // take care of other flags + // taken from 'undocumented z80 documented' and Fuse + FlagN = Regs[ALU].Bit(7); + FlagH = FlagC = ((Regs[ALU] + Regs[C] + 1) & 0xFF) < Regs[ALU]; + FlagP = TableParity[((Regs[ALU] + Regs[C] + 1) & 7) ^ Regs[B]]; + } + + Ztemp1 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp2 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp3 = Unsafe.Add(ref ci0, instr_pntr++); + + if ((Regs[B] != 0) && (Ztemp3 > 0)) + { + PopulateCURINSTR + (IDLE, + IDLE, + DEC16, PCl, PCh, + DEC16, PCl, PCh, + Ztemp2, L, H); + + PopulateBUSRQ(H, H, H, H, H); + PopulateMEMRQ(0, 0, 0, 0, 0); + IRQS = 5; + + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + I_skip = true; + } + else + { + if (Ztemp2 == INC16) { INC16_Func(L, H); } + else { DEC16_Func(L, H); } + } + break; + case REP_OP_O: + OUT_Func(Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++), Unsafe.Add(ref ci0, instr_pntr++)); + + Ztemp4 = Unsafe.Add(ref ci0, instr_pntr++); + if (Ztemp4 == DEC16) + { + DEC16_Func(L, H); + DEC8_Func(B); + TR16_Func(Z, W, C, B); + DEC16_Func(Z, W); + } + else + { + INC16_Func(L, H); + DEC8_Func(B); + TR16_Func(Z, W, C, B); + INC16_Func(Z, W); + } + + // take care of other flags + // taken from 'undocumented z80 documented' + FlagN = Regs[ALU].Bit(7); + FlagH = FlagC = (Regs[ALU] + Regs[L]) > 0xFF; + FlagP = TableParity[((Regs[ALU] + Regs[L]) & 7) ^ (Regs[B])]; + + Ztemp1 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp2 = Unsafe.Add(ref ci0, instr_pntr++); + Ztemp3 = Unsafe.Add(ref ci0, instr_pntr++); + + if ((Regs[B] != 0) && (Ztemp3 > 0)) + { + PopulateCURINSTR + (IDLE, + IDLE, + DEC16, PCl, PCh, + DEC16, PCl, PCh, + IDLE); + + PopulateBUSRQ(B, B, B, B, B); + PopulateMEMRQ(0, 0, 0, 0, 0); + IRQS = 5; + + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + I_skip = true; + } + break; + + case IORQ: + _link.IRQACKCallback(); + break; + } + + if (I_skip) + { + I_skip = false; + } + else if (++irq_pntr == IRQS) + { + if (EI_pending > 0) + { + EI_pending--; + if (EI_pending == 0) { IFF1 = IFF2 = true; } + } + + // NMI has priority + if (nonMaskableInterruptPending) + { + nonMaskableInterruptPending = false; + + TraceCallback?.Invoke(new(disassembly: "====NMI====", registerInfo: string.Empty)); + + iff2 = iff1; + iff1 = false; + NMI_(); + _link.NMICallback(); + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + + temp_R = (byte)(Regs[R] & 0x7F); + temp_R++; + temp_R &= 0x7F; + Regs[R] = (byte)((Regs[R] & 0x80) | temp_R); + + halted = false; + } + // if we are processing an interrrupt, we need to modify the instruction vector + else if (iff1 && FlagI) + { + iff1 = iff2 = false; + EI_pending = 0; + + TraceCallback?.Invoke(new(disassembly: "====IRQ====", registerInfo: string.Empty)); + + switch (interruptMode) + { + case 0: + // Requires something to be pushed onto the data bus + // we'll assume it's a zero for now + INTERRUPT_0(0); + break; + case 1: + INTERRUPT_1(); + break; + case 2: + INTERRUPT_2(); + break; + } + _link.IRQCallback(); + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + + temp_R = (byte)(Regs[R] & 0x7F); + temp_R++; + temp_R &= 0x7F; + Regs[R] = (byte)((Regs[R] & 0x80) | temp_R); + + halted = false; + } + // otherwise start a new normal access + else if (!halted) + { + PopulateCURINSTR + (IDLE, + WAIT, + OP_F, + OP); + + PopulateBUSRQ(PCh, 0, 0, 0); + PopulateMEMRQ(PCh, 0, 0, 0); + IRQS = 4; + + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + } + else + { + instr_pntr = mem_pntr = bus_pntr = irq_pntr = 0; + } + } + + TotalExecutedCycles++; + } + + // tracer stuff + public Action TraceCallback; + + public string TraceHeader => "Z80A: PC, machine code, mnemonic, operands, registers (AF, BC, DE, HL, IX, IY, SP, Cy), flags (CNP3H5ZS)"; + + public TraceInfo State(bool disassemble = true) + { + int bytes_read = 0; + + string disasm = disassemble ? Z80ADisassembler.Disassemble(RegPC, _link.ReadMemory, out bytes_read) : "---"; + string byte_code = null; + + for (ushort i = 0; i < bytes_read; i++) + { + byte_code += $"{_link.ReadMemory((ushort)(RegPC + i)):X2}"; + if (i < (bytes_read - 1)) + { + byte_code += " "; + } + } + + return new( + disassembly: $"{RegPC:X4}: {byte_code.PadRight(12)} {disasm.PadRight(26)}", + registerInfo: string.Join(" ", + $"AF:{(Regs[A] << 8) + Regs[F]:X4}", + $"BC:{(Regs[B] << 8) + Regs[C]:X4}", + $"DE:{(Regs[D] << 8) + Regs[E]:X4}", + $"HL:{(Regs[H] << 8) + Regs[L]:X4}", + $"IX:{(Regs[Ixh] << 8) + Regs[Ixl]:X4}", + $"IY:{(Regs[Iyh] << 8) + Regs[Iyl]:X4}", + $"SP:{Regs[SPl] | (Regs[SPh] << 8):X4}", + $"Cy:{TotalExecutedCycles}", + string.Concat( + FlagC ? "C" : "c", + FlagN ? "N" : "n", + FlagP ? "P" : "p", + Flag3 ? "3" : "-", + FlagH ? "H" : "h", + Flag5 ? "5" : "-", + FlagZ ? "Z" : "z", + FlagS ? "S" : "s", + FlagI ? "E" : "e"))); + } + + /// + /// Optimization method to set BUSRQ + /// + private void PopulateBUSRQ(ushort d0 = 0, ushort d1 = 0, ushort d2 = 0, ushort d3 = 0, ushort d4 = 0, ushort d5 = 0, ushort d6 = 0, ushort d7 = 0, ushort d8 = 0, + ushort d9 = 0, ushort d10 = 0, ushort d11 = 0, ushort d12 = 0, ushort d13 = 0, ushort d14 = 0, ushort d15 = 0, ushort d16 = 0, ushort d17 = 0, ushort d18 = 0) + { + BUSRQ[0] = d0; BUSRQ[1] = d1; BUSRQ[2] = d2; + BUSRQ[3] = d3; BUSRQ[4] = d4; BUSRQ[5] = d5; + BUSRQ[6] = d6; BUSRQ[7] = d7; BUSRQ[8] = d8; + BUSRQ[9] = d9; BUSRQ[10] = d10; BUSRQ[11] = d11; + BUSRQ[12] = d12; BUSRQ[13] = d13; BUSRQ[14] = d14; + BUSRQ[15] = d15; BUSRQ[16] = d16; BUSRQ[17] = d17; + BUSRQ[18] = d18; + } + + /// + /// Optimization method to set MEMRQ + /// + private void PopulateMEMRQ(ushort d0 = 0, ushort d1 = 0, ushort d2 = 0, ushort d3 = 0, ushort d4 = 0, ushort d5 = 0, ushort d6 = 0, ushort d7 = 0, ushort d8 = 0, + ushort d9 = 0, ushort d10 = 0, ushort d11 = 0, ushort d12 = 0, ushort d13 = 0, ushort d14 = 0, ushort d15 = 0, ushort d16 = 0, ushort d17 = 0, ushort d18 = 0) + { + MEMRQ[0] = d0; MEMRQ[1] = d1; MEMRQ[2] = d2; + MEMRQ[3] = d3; MEMRQ[4] = d4; MEMRQ[5] = d5; + MEMRQ[6] = d6; MEMRQ[7] = d7; MEMRQ[8] = d8; + MEMRQ[9] = d9; MEMRQ[10] = d10; MEMRQ[11] = d11; + MEMRQ[12] = d12; MEMRQ[13] = d13; MEMRQ[14] = d14; + MEMRQ[15] = d15; MEMRQ[16] = d16; MEMRQ[17] = d17; + MEMRQ[18] = d18; + } + + /// + /// Optimization method to set cur_instr + /// + private void PopulateCURINSTR(ushort d0 = 0, ushort d1 = 0, ushort d2 = 0, ushort d3 = 0, ushort d4 = 0, ushort d5 = 0, ushort d6 = 0, ushort d7 = 0, ushort d8 = 0, + ushort d9 = 0, ushort d10 = 0, ushort d11 = 0, ushort d12 = 0, ushort d13 = 0, ushort d14 = 0, ushort d15 = 0, ushort d16 = 0, ushort d17 = 0, ushort d18 = 0, + ushort d19 = 0, ushort d20 = 0, ushort d21 = 0, ushort d22 = 0, ushort d23 = 0, ushort d24 = 0, ushort d25 = 0, ushort d26 = 0, ushort d27 = 0, ushort d28 = 0, + ushort d29 = 0, ushort d30 = 0, ushort d31 = 0, ushort d32 = 0, ushort d33 = 0, ushort d34 = 0, ushort d35 = 0, ushort d36 = 0, ushort d37 = 0) + { + cur_instr[0] = d0; cur_instr[1] = d1; cur_instr[2] = d2; + cur_instr[3] = d3; cur_instr[4] = d4; cur_instr[5] = d5; + cur_instr[6] = d6; cur_instr[7] = d7; cur_instr[8] = d8; + cur_instr[9] = d9; cur_instr[10] = d10; cur_instr[11] = d11; + cur_instr[12] = d12; cur_instr[13] = d13; cur_instr[14] = d14; + cur_instr[15] = d15; cur_instr[16] = d16; cur_instr[17] = d17; + cur_instr[18] = d18; cur_instr[19] = d19; cur_instr[20] = d20; + cur_instr[21] = d21; cur_instr[22] = d22; cur_instr[23] = d23; + cur_instr[24] = d24; cur_instr[25] = d25; cur_instr[26] = d26; + cur_instr[27] = d27; cur_instr[28] = d28; cur_instr[29] = d29; + cur_instr[30] = d30; cur_instr[31] = d31; cur_instr[32] = d32; + cur_instr[33] = d33; cur_instr[34] = d34; cur_instr[35] = d35; + cur_instr[36] = d36; cur_instr[37] = d37; + } + + // State Save/Load + public void SyncState(Serializer ser) + { + ser.BeginSection(nameof(Z80AOpt)); + ser.Sync(nameof(Regs), ref Regs, false); + ser.Sync("NMI", ref nonMaskableInterrupt); + ser.Sync("NMIPending", ref nonMaskableInterruptPending); + ser.Sync("IM", ref interruptMode); + ser.Sync("IFF1", ref iff1); + ser.Sync("IFF2", ref iff2); + ser.Sync("Halted", ref halted); + ser.Sync(nameof(I_skip), ref I_skip); + ser.Sync("ExecutedCycles", ref TotalExecutedCycles); + ser.Sync(nameof(EI_pending), ref EI_pending); + + ser.Sync(nameof(instr_pntr), ref instr_pntr); + ser.Sync(nameof(bus_pntr), ref bus_pntr); + ser.Sync(nameof(mem_pntr), ref mem_pntr); + ser.Sync(nameof(irq_pntr), ref irq_pntr); + ser.Sync(nameof(cur_instr), ref cur_instr, false); + ser.Sync(nameof(BUSRQ), ref BUSRQ, false); + ser.Sync(nameof(IRQS), ref IRQS); + ser.Sync(nameof(MEMRQ), ref MEMRQ, false); + ser.Sync(nameof(opcode), ref opcode); + ser.Sync(nameof(FlagI), ref FlagI); + ser.Sync(nameof(FlagW), ref FlagW); + + ser.Sync(nameof(NO_prefix), ref NO_prefix); + ser.Sync(nameof(CB_prefix), ref CB_prefix); + ser.Sync(nameof(IX_prefix), ref IX_prefix); + ser.Sync(nameof(IY_prefix), ref IY_prefix); + ser.Sync(nameof(IXCB_prefix), ref IXCB_prefix); + ser.Sync(nameof(IYCB_prefix), ref IYCB_prefix); + ser.Sync(nameof(EXTD_prefix), ref EXTD_prefix); + ser.Sync(nameof(PRE_SRC), ref PRE_SRC); + + ser.EndSection(); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IFDDHost.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IFDDHost.cs deleted file mode 100644 index 6e3f6745211..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IFDDHost.cs +++ /dev/null @@ -1,29 +0,0 @@ - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Defines an object that can load a floppy disk image - /// - public interface IFDDHost - { - /// - /// The currently inserted diskimage - /// - FloppyDisk Disk { get; set; } - - /// - /// Parses a new disk image and loads it into this floppy drive - /// - void FDD_LoadDisk(byte[] diskData); - - /// - /// Ejects the current disk - /// - void FDD_EjectDisk(); - - /// - /// Signs whether the current active drive has a disk inserted - /// - bool FDD_IsDiskLoaded { get; } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs index cd50120130e..c1c24cff7fa 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs @@ -4,9 +4,10 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// - /// Represents a PSG device (in this case an AY-3-891x) + /// Represents a PSG device (in this case an AY-3-891x). IDisposable so the host can release the + /// shared AY391x core's unmanaged BlipBuffers on core teardown. /// - public interface IPSG : ISoundProvider, IPortIODevice + public interface IPSG : ISoundProvider, IPortIODevice, IDisposable { /// /// Initlization routine diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs index 5a73a99cf73..f0445202dd7 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs @@ -1,22 +1,31 @@ -using BizHawk.Common; -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Common; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; -using System.Linq; -using System.Text; using BizHawk.Emulation.Cores.Sound; +using BizHawk.Emulation.Cores.Tapes; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// - /// Represents the tape device (or build-in datacorder as it was called +2 and above) + /// Represents the tape device (or built-in datacorder as it was called +2 and above). + /// + /// The generic tape mechanism (block model, transport, EAR signal generation) lives in the shared + /// TapeDeck; this class owns a deck and supplies the Spectrum-specific pieces via + /// ITapeHost: the Z80 cycle counter, the tape beeper, on-screen notifications, tape-format + /// loading, the ROM auto-detect (tape-trap) monitor, the flash loader and the tape input/output ports. /// - public sealed class DatacorderDevice : IPortIODevice + public sealed class DatacorderDevice : IPortIODevice, ITapeHost { private SpectrumBase _machine { get; set; } - private Z80A _cpu { get; set; } + private Z80AOpt _cpu { get; set; } private OneBitBeeper _buzzer { get; set; } + /// + /// The shared tape player that holds the loaded blocks and generates the signal. + /// + private TapeDeck _deck; + /// /// Default constructor /// @@ -33,248 +42,70 @@ public void Init(SpectrumBase machine) _machine = machine; _cpu = _machine.CPU; _buzzer = machine.TapeBuzzer; - } - - /// - /// Internal counter used to trigger tape buzzer output - /// - private int counter = 0; - /// - /// The index of the current tape data block that is loaded - /// - private int _currentDataBlockIndex = 0; - public int CurrentDataBlockIndex - { - get - { - if (_dataBlocks.Count > 0) { return _currentDataBlockIndex; } - else { return -1; } - } - set - { - if (value == _currentDataBlockIndex) { return; } - if (value < _dataBlocks.Count && value >= 0) - { - _currentDataBlockIndex = value; - _position = 0; - } - } + // Tape formats (TAP/TZX/PZX and CPC CDT) store pulse timings against a 3.5MHz reference. Scale them + // into this model's CPU clock so the tape plays at the correct rate regardless of model: the 48K is + // 3.5MHz exactly (ratio 1.0, unchanged), the 128K family is 3.5469MHz (ratio ~1.0134). Without this + // the 128K plays the same tape ~1.3% faster than the 48K (its higher clock burns the unscaled + // periods quicker), which does not happen on real hardware where the tape is an external source. + _deck = new TapeDeck(this, _machine.ULADevice.ClockSpeed / 3_500_000.0); } - /// - /// The current position within the current data block - /// - private int _position = 0; - public int Position - { - get - { - if (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) { return 0; } - - return _position; - } - } + // -- shared deck proxies (external callers still talk to DatacorderDevice) -- - /// - /// Signs whether the tape is currently playing or not - /// - private bool _tapeIsPlaying = false; - public bool TapeIsPlaying => _tapeIsPlaying; - - /// - /// A list of the currently loaded data blocks - /// - private List _dataBlocks = new List(); public List DataBlocks { - get => _dataBlocks; - set => _dataBlocks = value; + get => _deck.DataBlocks; + set => _deck.DataBlocks = value; } /// - /// Stores the last CPU t-state value - /// - private long _lastCycle = 0; - - /// - /// Edge - /// - private int _waitEdge = 0; - - /// - /// Current tapebit state + /// The loading / copy-protection scheme detected from the currently loaded tape (report-only). /// - private bool currentState = false; + public TapeProtectionScheme DetectedLoader => TapeProtection.Detect(_deck.DataBlocks); /// - /// Signs whether the device should autodetect when the Z80 has entered into - /// 'load' mode and auto-play the tape if neccesary + /// Human-readable name of DetectedLoader for the OSD. /// - private bool _autoPlay; + public string DetectedLoaderName => TapeProtection.DisplayName(DetectedLoader); - /// - /// Should be fired at the end of every frame - /// Primary purpose is to detect tape traps and manage auto play - /// - public void EndFrame() + public int CurrentDataBlockIndex { - MonitorFrame(); + get => _deck.CurrentDataBlockIndex; + set => _deck.CurrentDataBlockIndex = value; } - /// - /// Starts the tape playing from the beginning of the current block - /// - public void Play() - { - if (_tapeIsPlaying) - return; - - _machine.Spectrum.OSD_TapePlaying(); + public int Position => _deck.Position; - // update the lastCycle - _lastCycle = _cpu.TotalExecutedCycles; + public bool TapeIsPlaying => _deck.TapeIsPlaying; - // reset waitEdge and position - _waitEdge = 0; - _position = 0; + public void Play() => _deck.Play(); - if (_dataBlocks.Count > 0 && _currentDataBlockIndex >= 0) //TODO removed a comment that said "index is 1 or greater", but code is clearly "0 or greater"--which is correct? --yoshi - { - while (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) - { - // we are at the end of a data block - move to the next - _position = 0; - _currentDataBlockIndex++; + public void Stop() => _deck.Stop(); - // are we at the end of the tape? - if (_currentDataBlockIndex >= _dataBlocks.Count) - { - break; - } - } + public void RTZ() => _deck.RTZ(); - // check for end of tape - if (_currentDataBlockIndex >= _dataBlocks.Count) - { - // end of tape reached. Rewind to beginning - AutoStopTape(); - RTZ(); - return; - } + public void SkipBlock(bool skipForward) => _deck.SkipBlock(skipForward); - // update waitEdge with the current position in the current block - _waitEdge = _dataBlocks[_currentDataBlockIndex].DataPeriods[_position]; + public void TapeCycle() => _deck.TapeCycle(); - // sign that the tape is now playing - _tapeIsPlaying = true; - } - } + public void Reset() => _deck.Reset(); - /// - /// Stops the tape - /// (should move to the beginning of the next block) - /// - public void Stop() - { - if (!_tapeIsPlaying) - return; - - _machine.Spectrum.OSD_TapeStopped(); - - // sign that the tape is no longer playing - _tapeIsPlaying = false; - - if (_currentDataBlockIndex >= 0 // we are at datablock 1 or above //TODO 1-indexed then? --yoshi - && _position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count - 1) // the block is still playing back - { - // move to the next block - _currentDataBlockIndex++; - - if (_currentDataBlockIndex >= _dataBlocks.Count) - { - _currentDataBlockIndex = -1; - } - - // reset waitEdge and position - _waitEdge = 0; - _position = 0; - - if (_currentDataBlockIndex < 0 && _dataBlocks.Count > 0) //TODO deleted a comment that said "block index is -1", but code is clearly "is negative"--are lower values not reachable? --yoshi - { - // move the index on to 0 - _currentDataBlockIndex = 0; - } - } - - // update the lastCycle - _lastCycle = _cpu.TotalExecutedCycles; - } + public bool GetEarBit(long cpuCycle) => _deck.GetEarBit(cpuCycle); /// - /// Rewinds the tape to it's beginning (return to zero) + /// Signs whether the device should autodetect when the Z80 has entered into + /// 'load' mode and auto-play the tape if neccesary /// - public void RTZ() - { - Stop(); - _machine.Spectrum.OSD_TapeRTZ(); - _currentDataBlockIndex = 0; - } + private bool _autoPlay; /// - /// Performs a block skip operation on the current tape - /// TRUE: skip forward - /// FALSE: skip backward + /// Should be fired at the end of every frame + /// Primary purpose is to detect tape traps and manage auto play /// - public void SkipBlock(bool skipForward) + public void EndFrame() { - int blockCount = _dataBlocks.Count; - int targetBlockId = _currentDataBlockIndex; - - if (skipForward) - { - if (_currentDataBlockIndex == blockCount - 1) - { - // last block, go back to beginning - targetBlockId = 0; - } - else - { - targetBlockId++; - } - } - else - { - if (_currentDataBlockIndex == 0) - { - // already first block, goto last block - targetBlockId = blockCount - 1; - } - else - { - targetBlockId--; - } - } - - var bl = _dataBlocks[targetBlockId]; - - StringBuilder sbd = new StringBuilder(); - sbd.Append('('); - sbd.Append((targetBlockId + 1) + " of " + _dataBlocks.Count); - sbd.Append(") : "); - sbd.Append(bl.BlockDescription); - if (bl.MetaData.Count > 0) - { - sbd.Append(" - "); - sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); - } - - if (skipForward) - _machine.Spectrum.OSD_TapeNextBlock(sbd.ToString()); - else - _machine.Spectrum.OSD_TapePrevBlock(sbd.ToString()); - - CurrentDataBlockIndex = targetBlockId; + MonitorFrame(); } /// @@ -379,190 +210,6 @@ public void LoadTape(byte[] tapeData) } } - /// - /// Resets the tape - /// - public void Reset() - { - RTZ(); - } - - /// - /// Is called every cpu cycle but runs every 50 t-states - /// This enables the tape devices to play out even if the spectrum itself is not - /// requesting tape data - /// - public void TapeCycle() - { - if (TapeIsPlaying) - { - counter++; - - if (counter > 20) - { - counter = 0; - bool state = GetEarBit(_machine.CPU.TotalExecutedCycles); - _buzzer.ProcessPulseValue(state); - } - } - } - - /// - /// Simulates the spectrum 'EAR' input reading data from the tape - /// - public bool GetEarBit(long cpuCycle) - { - // decide how many cycles worth of data we are capturing - long cycles = cpuCycle - _lastCycle; - - bool is48k = _machine.IsIn48kMode(); - - // check whether tape is actually playing - if (!_tapeIsPlaying) - { - // it's not playing. Update lastCycle and return - _lastCycle = cpuCycle; - return false; - } - - // check for end of tape - if (_currentDataBlockIndex < 0) - { - // end of tape reached - RTZ (and stop) - RTZ(); - return currentState; - } - - // process the cycles based on the waitEdge - while (cycles >= _waitEdge) - { - // decrement cycles - cycles -= _waitEdge; - - if (_position == 0 && _tapeIsPlaying) - { - // notify about the current block - var bl = _dataBlocks[_currentDataBlockIndex]; - - StringBuilder sbd = new StringBuilder(); - sbd.Append('('); - sbd.Append((_currentDataBlockIndex + 1) + " of " + _dataBlocks.Count); - sbd.Append(") : "); - sbd.Append(bl.BlockDescription); - if (bl.MetaData.Count > 0) - { - sbd.Append(" - "); - sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); - } - _machine.Spectrum.OSD_TapePlayingBlockInfo(sbd.ToString()); - } - - // increment the current period position - _position++; - - if (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) - { - // we have reached the end of the current block - if (_dataBlocks[_currentDataBlockIndex].DataPeriods.Count == 0) - { - // notify about the current block (we are skipping it because its empty) - var bl = _dataBlocks[_currentDataBlockIndex]; - StringBuilder sbd = new StringBuilder(); - sbd.Append('('); - sbd.Append((_currentDataBlockIndex + 1) + " of " + _dataBlocks.Count); - sbd.Append(") : "); - sbd.Append(bl.BlockDescription); - if (bl.MetaData.Count > 0) - { - sbd.Append(" - "); - sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); - } - _machine.Spectrum.OSD_TapePlayingSkipBlockInfo(sbd.ToString()); - } - - // skip any empty blocks (and process any command blocks) - while (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) - { - // check for any commands - var command = _dataBlocks[_currentDataBlockIndex].Command; - var block = _dataBlocks[_currentDataBlockIndex]; - bool shouldStop = false; - switch (command) - { - // Stop the tape command found - if this is the end of the tape RTZ - // otherwise just STOP and move to the next block - case TapeCommand.STOP_THE_TAPE: - - _machine.Spectrum.OSD_TapeStoppedAuto(); - shouldStop = true; - - if (_currentDataBlockIndex >= _dataBlocks.Count) - RTZ(); - else - { - Stop(); - } - - _monitorTimeOut = 2000; - break; - case TapeCommand.STOP_THE_TAPE_48K: - if (is48k) - { - _machine.Spectrum.OSD_TapeStoppedAuto(); - shouldStop = true; - - if (_currentDataBlockIndex >= _dataBlocks.Count) - RTZ(); - else - { - Stop(); - } - - _monitorTimeOut = 2000; - } - break; - } - - if (shouldStop) - break; - - _position = 0; - _currentDataBlockIndex++; - - if (_currentDataBlockIndex >= _dataBlocks.Count) - { - break; - } - } - - // check for end of tape - if (_currentDataBlockIndex >= _dataBlocks.Count) - { - _currentDataBlockIndex = -1; - RTZ(); - return currentState; - } - } - - // update waitEdge with current position within the current block - _waitEdge = _dataBlocks[_currentDataBlockIndex].DataPeriods.Count > 0 ? _dataBlocks[_currentDataBlockIndex].DataPeriods[_position] : 0; - - // flip the current state - //FlipTapeState(); - currentState = _dataBlocks[_currentDataBlockIndex].DataLevels[_position]; - } - - // update lastCycle and return currentstate - _lastCycle = cpuCycle - cycles; - - return currentState; - } - - private void FlipTapeState() - { - currentState = !currentState; - } - /// /// Flash loading implementation /// (Deterministic Emulation must be FALSE) @@ -576,10 +223,10 @@ private bool FlashLoad() var util = _machine.Spectrum; - if (_currentDataBlockIndex < 0) - _currentDataBlockIndex = 0; + if (CurrentDataBlockIndex < 0) + CurrentDataBlockIndex = 0; - if (_currentDataBlockIndex >= DataBlocks.Count) + if (CurrentDataBlockIndex >= DataBlocks.Count) return false; //var val = GetEarBit(_cpu.TotalExecutedCycles); @@ -587,7 +234,7 @@ private bool FlashLoad() ushort addr = _cpu.RegPC; - var tb = DataBlocks[_currentDataBlockIndex]; + var tb = DataBlocks[CurrentDataBlockIndex]; var tData = tb.BlockData; if (tData == null || tData.Length < 2) @@ -652,7 +299,7 @@ private bool FlashLoad() util.SetCpuRegister("PC", pc); - _currentDataBlockIndex++; + CurrentDataBlockIndex++; return true; } @@ -714,7 +361,7 @@ public void MonitorRead() if (_monitorCount >= 16 && _autoPlay) { - if (!_tapeIsPlaying) + if (!TapeIsPlaying) { Play(); _machine.Spectrum.OSD_TapePlayingAuto(); @@ -735,7 +382,7 @@ public void MonitorRead() public void AutoStopTape() { - if (!_tapeIsPlaying) + if (!TapeIsPlaying) return; if (!_autoPlay) @@ -747,7 +394,7 @@ public void AutoStopTape() public void AutoStartTape() { - if (_tapeIsPlaying) + if (TapeIsPlaying) return; if (!_autoPlay) @@ -761,10 +408,10 @@ public void AutoStartTape() private void MonitorFrame() { - if (_tapeIsPlaying && _autoPlay) + if (TapeIsPlaying && _autoPlay) { if (DataBlocks.Count > 1 - || _dataBlocks[_currentDataBlockIndex].BlockDescription is not (BlockType.CSW_Recording or BlockType.WAV_Recording)) + || DataBlocks[CurrentDataBlockIndex].BlockDescription is not (BlockType.CSW_Recording or BlockType.WAV_Recording)) { // we should only stop the tape when there are multiple blocks // if we just have one big block (maybe a CSW or WAV) then auto stopping will cock things up @@ -773,7 +420,7 @@ private void MonitorFrame() if (_monitorTimeOut < 0) { - if (_dataBlocks[_currentDataBlockIndex].BlockDescription is not (BlockType.PAUSE_BLOCK or BlockType.PAUS)) + if (DataBlocks[CurrentDataBlockIndex].BlockDescription is not (BlockType.PAUSE_BLOCK or BlockType.PAUS)) { AutoStopTape(); } @@ -787,7 +434,7 @@ private void MonitorFrame() long diff = _machine.CPU.TotalExecutedCycles - _lastINCycle; // get current datablock - var block = DataBlocks[_currentDataBlockIndex]; + var block = DataBlocks[CurrentDataBlockIndex]; // is this a pause block? if (block.BlockDescription is BlockType.PAUS or BlockType.PAUSE_BLOCK) @@ -808,7 +455,7 @@ private void MonitorFrame() // don't autostop if there is only 1 block if (DataBlocks.Count is 1 - || _dataBlocks[_currentDataBlockIndex].BlockDescription is BlockType.CSW_Recording or BlockType.WAV_Recording) + || DataBlocks[CurrentDataBlockIndex].BlockDescription is BlockType.CSW_Recording or BlockType.WAV_Recording) { return; } @@ -818,7 +465,7 @@ private void MonitorFrame() // There have been no attempted tape reads by the CPU within the double timeout period // Autostop the tape AutoStopTape(); - _lastCycle = _cpu.TotalExecutedCycles; + _deck.ResetLastCycleToNow(); } } } @@ -837,9 +484,9 @@ public bool ReadPort(ushort port, ref int result) { if (TapeIsPlaying) { - GetEarBit(_cpu.TotalExecutedCycles); + _deck.GetEarBit(_cpu.TotalExecutedCycles); } - if (currentState) + if (_deck.CurrentEarLevel) { result |= TAPE_BIT; } @@ -866,7 +513,7 @@ public bool WritePort(ushort port, int result) { if (!TapeIsPlaying) { - currentState = ((byte)result & 0x10) != 0; + _deck.CurrentEarLevel = ((byte)result & 0x10) != 0; } return true; @@ -878,13 +525,9 @@ public bool WritePort(ushort port, int result) public void SyncState(Serializer ser) { ser.BeginSection(nameof(DatacorderDevice)); - ser.Sync(nameof(counter), ref counter); - ser.Sync(nameof(_currentDataBlockIndex), ref _currentDataBlockIndex); - ser.Sync(nameof(_position), ref _position); - ser.Sync(nameof(_tapeIsPlaying), ref _tapeIsPlaying); - ser.Sync(nameof(_lastCycle), ref _lastCycle); - ser.Sync(nameof(_waitEdge), ref _waitEdge); - ser.Sync(nameof(currentState), ref currentState); + // transport/signal state (field names + order preserve the pre-extraction savestate layout) + _deck.SyncState(ser); + // auto-detect (tape-trap) monitor state ser.Sync(nameof(_lastINCycle), ref _lastINCycle); ser.Sync(nameof(_monitorCount), ref _monitorCount); ser.Sync(nameof(_monitorTimeOut), ref _monitorTimeOut); @@ -892,5 +535,38 @@ public void SyncState(Serializer ser) ser.Sync(nameof(_monitorLastRegs), ref _monitorLastRegs, false); ser.EndSection(); } + + // -- ITapeHost: the services the shared deck needs from the Spectrum -- + + long ITapeHost.TotalExecutedCycles => _cpu.TotalExecutedCycles; + + bool ITapeHost.IsIn48kMode => _machine.IsIn48kMode(); + + bool ITapeHost.FastLoadAllowed => !_machine.Spectrum.DeterministicEmulation; + + void ITapeHost.FeedBeeper(bool earLevel) + { + // event-driven clock: position the beeper at the current frame cycle before the pulse + _buzzer.SetClock((int)_machine.CurrentFrameCycle); + _buzzer.ProcessPulseValue(earLevel); + } + + void ITapeHost.NotifyPlay() => _machine.Spectrum.OSD_TapePlaying(); + + void ITapeHost.NotifyStop() => _machine.Spectrum.OSD_TapeStopped(); + + void ITapeHost.NotifyRewind() => _machine.Spectrum.OSD_TapeRTZ(); + + void ITapeHost.NotifyNextBlock(string blockInfo) => _machine.Spectrum.OSD_TapeNextBlock(blockInfo); + + void ITapeHost.NotifyPrevBlock(string blockInfo) => _machine.Spectrum.OSD_TapePrevBlock(blockInfo); + + void ITapeHost.NotifyPlayingBlock(string blockInfo) => _machine.Spectrum.OSD_TapePlayingBlockInfo(blockInfo); + + void ITapeHost.NotifySkipBlock(string blockInfo) => _machine.Spectrum.OSD_TapePlayingSkipBlockInfo(blockInfo); + + void ITapeHost.NotifyStoppedAuto() => _machine.Spectrum.OSD_TapeStoppedAuto(); + + void ITapeHost.NotifyStopCommand() => _monitorTimeOut = 2000; } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/BetaDiskController.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/BetaDiskController.cs new file mode 100644 index 00000000000..c090f3063d6 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/BetaDiskController.cs @@ -0,0 +1,170 @@ +using BizHawk.Common; +using BizHawk.Emulation.Cores.Floppy; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The Beta 128 disk interface built into the Pentagon (and Scorpion), backed by the shared flux + /// subsystem: a timed WD1793 controller driving a double-sided 80-track DD drive over a flux/cell disk + /// model. TR-DOS .TRD images are converted to flux on load. Like the +3's uPD765 the WD1793's INTRQ is + /// not wired to the Z80 (TR-DOS polls the system port for INTRQ/DRQ), so the controller is advanced + /// lazily on each port access from the CPU cycle count, keeping the byte cadence in step with the poll. + /// Port decode (partial): the interface only responds when A0-A4 are all high (low five bits = 0x1F), + /// which is what separates it from the ULA at 0xFE. A7 then selects the system latch (0xFF) from the + /// WD register file, and A6/A5 select the register: 0x1F command/status, 0x3F track, 0x5F sector, + /// 0x7F data. The whole interface is gated by the caller on TR-DOS being paged in. + /// + public sealed class BetaDiskController : IFloppyDiskController + { + /// + /// The physical drive the Beta 128 is fitted with. The Pentagon shipped with both 5.25" and 3.5" + /// double-density drives. Both spin at 300 RPM (so the index period and 250 kbit/s data rate - the + /// timings the WD1793 and copy protection actually depend on - are identical), and seek timing is + /// dominated by the WD1793's programmed step rate rather than the drive mechanics; the drive type + /// therefore only tweaks the secondary spin-up and track-to-track/settle floors. Both are modelled as + /// 80-cylinder double-sided so they can read any TR-DOS geometry (40-track disks included). + /// + public enum DriveKind { ThreeHalfInch, FiveQuarterInch } + + public DriveKind DriveType { get; set; } = DriveKind.ThreeHalfInch; + + private static FloppyDriveProfile Profile(DriveKind kind) => kind == DriveKind.FiveQuarterInch + ? new FloppyDriveProfile { Cylinders = 80, Sides = 2, Rpm = 300, SpinUpMs = 750, TrackToTrackMs = 6, SettleMs = 15 } + : new FloppyDriveProfile { Cylinders = 80, Sides = 2, Rpm = 300, SpinUpMs = 400, TrackToTrackMs = 3, SettleMs = 15 }; + + private SpectrumBase _machine; + private readonly Wd1793Fdc _fdc = new(); + private FloppyDrive _drive; + private long _lastCycle; + private bool _diskLoaded; + private byte _system; // last value written to the 0xFF system latch + + public bool FDD_FLAG_MOTOR { get; set; } + + public void Init(SpectrumBase machine) + { + _machine = machine; + _drive = new FloppyDrive(Profile(DriveType)); + _fdc.Drives[0] = _drive; + _fdc.ConfigureTiming(machine.ULADevice.ClockSpeed); + _fdc.Reset(); + _lastCycle = Cycles; + } + + private long Cycles => _machine?.CPU.TotalExecutedCycles ?? 0; + + // Advance the controller to "now" (TR-DOS polls, so this runs on every Beta port access). + private void CatchUp() + { + long delta = Cycles - _lastCycle; + _lastCycle = Cycles; + while (delta > 0) + { + int step = (int)System.Math.Min(delta, 100_000); + _fdc.Clock(step); + delta -= step; + } + } + + // Returns true and sets 'system'/'reg' when the port addresses the Beta interface. + private static bool Decode(ushort port, out bool system, out int reg) + { + system = false; reg = 0; + byte lb = (byte)(port & 0xFF); + if ((lb & 0x1F) != 0x1F) return false; // A0-A4 must all be high + if ((lb & 0x80) != 0) { system = true; return true; } // A7 = system latch (0xFF) + reg = (lb >> 5) & 0x03; // A6,A5 = register select + return true; + } + + public bool ReadPort(ushort port, ref int result) + { + if (!Decode(port, out bool system, out int reg)) return false; + CatchUp(); + if (system) + { + // system-port read: INTRQ on bit 7, DRQ on bit 6 (what TR-DOS polls); other bits pulled high + result = (_fdc.IntRequest ? 0x80 : 0x00) | (_fdc.DataRequest ? 0x40 : 0x00) | 0x3F; + return true; + } + result = reg switch + { + 0 => _fdc.ReadStatus(), + 1 => _fdc.ReadTrack(), + 2 => _fdc.ReadSector(), + _ => _fdc.ReadData(), + }; + return true; + } + + public bool WritePort(ushort port, int value) + { + if (!Decode(port, out bool system, out int reg)) return false; + CatchUp(); + byte v = (byte)value; + if (system) + { + _system = v; + // bits 0-1 drive select, bit 2 reset (0 = reset), bit 4 side, bit 6 density (0 = double). + // The side line is active-low: bit 4 = 1 selects side 0, bit 4 = 0 selects side 1 (confirmed + // by tracing TR-DOS - it writes bit 4 = 1 to read the disk-info/catalogue on physical side 0). + int drive = v & 0x03; + int side = ((v >> 4) & 0x01) ^ 1; + bool doubleDensity = (v & 0x40) == 0; + _fdc.SetSystem(drive, side, doubleDensity); + FDD_FLAG_MOTOR = true; + _drive.MotorOn = true; + if ((v & 0x04) == 0) _fdc.Reset(); // MR asserted low + return true; + } + switch (reg) + { + case 0: _fdc.WriteCommand(v); break; + case 1: _fdc.WriteTrack(v); break; + case 2: _fdc.WriteSector(v); break; + default: _fdc.WriteData(v); break; + } + return true; + } + + public void FDD_LoadDisk(byte[] diskData, int side) + { + FluxDisk flux; + if (SclConverter.IsScl(diskData)) flux = SclConverter.ToFluxDisk(diskData); + else if (TrdConverter.IsTrd(diskData)) flux = TrdConverter.ToFluxDisk(diskData); + else flux = DiskImageLoader.ToFluxDisk(diskData); // SCP/HFE/FDI/etc + if (side >= 0 && flux.Sides > 1) flux = flux.ExtractSide(side); + _drive.Disk = flux; + _diskLoaded = _drive.Disk != null; + } + + public void FDD_EjectDisk() + { + _drive.Disk = null; + _diskLoaded = false; + } + + public bool FDD_IsDiskLoaded => _drive.Disk != null; + public bool DiskInserted => _drive.Disk != null; + public bool DriveLight => FDD_FLAG_MOTOR && _fdc.Active; + + /// + /// TR-DOS disks do not carry the +3-style flux copy-protection the detector recognises. + /// + public string ProtectionName => "None"; + + public void SyncState(Serializer ser) + { + ser.BeginSection("BetaDiskController"); + bool motor = FDD_FLAG_MOTOR; + ser.Sync(nameof(FDD_FLAG_MOTOR), ref motor); + FDD_FLAG_MOTOR = motor; + ser.Sync(nameof(_lastCycle), ref _lastCycle); + ser.Sync(nameof(_diskLoaded), ref _diskLoaded); + ser.Sync(nameof(_system), ref _system); + _fdc.SyncState(ser); + _drive.SyncState(ser); + ser.EndSection(); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/CHRN.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/CHRN.cs deleted file mode 100644 index 33cb8e48d8e..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/CHRN.cs +++ /dev/null @@ -1,176 +0,0 @@ - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Used for the sector CHRN structure - /// - public class CHRN - { - /// - /// Track - /// - public byte C { get; set; } - - /// - /// Side - /// - public byte H { get; set; } - - /// - /// Sector ID - /// - public byte R { get; set; } - - /// - /// Sector Size - /// - public byte N { get; set; } - - /// - /// Status register 1 - /// - private byte _flag1; - public byte Flag1 - { - get => _flag1; - set => _flag1 = value; - } - - /// - /// Status register 2 - /// - private byte _flag2; - public byte Flag2 - { - get => _flag2; - set => _flag2 = value; - } - - /// - /// Used to store the last transmitted/received data bytes - /// - public byte[] DataBytes { get; set; } - - /// - /// ID for the read/write data command - /// - public int DataID { get; set; } - - /// - /// Missing Address Mark (Sector_ID or DAM not found) - /// - public bool ST1MA - { - get => NECUPD765.GetBit(0, _flag1); - set - { - if (value) { NECUPD765.SetBit(0, ref _flag1); } - else { NECUPD765.UnSetBit(0, ref _flag1); } - } - } - - /// - /// No Data (Sector_ID not found, CRC fail in ID_field) - /// - public bool ST1ND - { - get => NECUPD765.GetBit(2, _flag1); - set - { - if (value) { NECUPD765.SetBit(2, ref _flag1); } - else { NECUPD765.UnSetBit(2, ref _flag1); } - } - } - - /// - /// Data Error (CRC-fail in ID- or Data-Field) - /// - public bool ST1DE - { - get => NECUPD765.GetBit(5, _flag1); - set - { - if (value) { NECUPD765.SetBit(5, ref _flag1); } - else { NECUPD765.UnSetBit(5, ref _flag1); } - } - } - - /// - /// End of Track (set past most read/write commands) (see IC) - /// - public bool ST1EN - { - get => NECUPD765.GetBit(7, _flag1); - set - { - if (value) { NECUPD765.SetBit(7, ref _flag1); } - else { NECUPD765.UnSetBit(7, ref _flag1); } - } - } - - /// - /// Missing Address Mark in Data Field (DAM not found) - /// - public bool ST2MD - { - get => NECUPD765.GetBit(0, _flag2); - set - { - if (value) { NECUPD765.SetBit(0, ref _flag2); } - else { NECUPD765.UnSetBit(0, ref _flag2); } - } - } - - /// - /// Bad Cylinder (read/programmed track-ID different and read-ID = FF) - /// - public bool ST2BC - { - get => NECUPD765.GetBit(1, _flag2); - set - { - if (value) { NECUPD765.SetBit(1, ref _flag2); } - else { NECUPD765.UnSetBit(1, ref _flag2); } - } - } - - /// - /// Wrong Cylinder (read/programmed track-ID different) (see b1) - /// - public bool ST2WC - { - get => NECUPD765.GetBit(4, _flag2); - set - { - if (value) { NECUPD765.SetBit(4, ref _flag2); } - else { NECUPD765.UnSetBit(4, ref _flag2); } - } - } - - /// - /// Data Error in Data Field (CRC-fail in data-field) - /// - public bool ST2DD - { - get => NECUPD765.GetBit(5, _flag2); - set - { - if (value) { NECUPD765.SetBit(5, ref _flag2); } - else { NECUPD765.UnSetBit(5, ref _flag2); } - } - } - - /// - /// Control Mark (read/scan command found sector with deleted DAM) - /// - public bool ST2CM - { - get => NECUPD765.GetBit(6, _flag2); - set - { - if (value) { NECUPD765.SetBit(6, ref _flag2); } - else { NECUPD765.UnSetBit(6, ref _flag2); } - } - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/IFloppyDiskController.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/IFloppyDiskController.cs new file mode 100644 index 00000000000..905ed0a4b33 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/IFloppyDiskController.cs @@ -0,0 +1,55 @@ +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The floppy disk controller surface the +3 machine drives. Implemented by the shared flux-based uPD765 + /// subsystem adapter. This is the seam that lets the machine stay independent of the controller internals. + /// + public interface IFloppyDiskController : IPortIODevice + { + /// + /// Wire the controller to its host machine (for the CPU cycle clock). + /// + void Init(SpectrumBase machine); + + /// + /// Load a disk image (any supported format) into drive 0. side selects one side of + /// a double-sided image (0 or 1) to present as a single-sided disk for the single-headed drive; -1 + /// loads the image as-is. + /// + void FDD_LoadDisk(byte[] diskData, int side); + + /// + /// Eject the disk in drive 0. + /// + void FDD_EjectDisk(); + + /// + /// True if drive 0 has a disk inserted. + /// + bool FDD_IsDiskLoaded { get; } + + /// + /// The spindle-motor line (port 0x1ffd bit 3), also read back for snapshots. + /// + bool FDD_FLAG_MOTOR { get; set; } + + /// + /// Disk activity indicator for the UI light. + /// + bool DriveLight { get; } + + /// + /// True if a disk is present (for status messages). + /// + bool DiskInserted { get; } + + /// + /// Human-readable protection description for status messages. + /// + string ProtectionName { get; } + + void SyncState(Serializer ser); + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.Definitions.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.Definitions.cs deleted file mode 100644 index 43e3885d4cb..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.Definitions.cs +++ /dev/null @@ -1,809 +0,0 @@ -using BizHawk.Common; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Definitions - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 - { - /// - /// Defines the current phase of the controller - /// - private enum Phase - { - /// - /// FDC is in an idle state, awaiting the next initial command byte - /// - Idle, - - /// - /// FDC is in a state waiting for the next command instruction - /// A command consists of a command byte (eventually including the MF, MK, SK bits), and up to eight parameter bytes - /// - Command, - - /// - /// During this phase, the actual data is transferred (if any). Usually that are the data bytes for the read/written sector(s), except for the Format Track Command, - /// in that case four bytes for each sector are transferred - /// - Execution, - - /// - /// Returns up to seven result bytes (depending on the command) that are containing status information. The Recalibrate and Seek Track commands do not return result bytes directly, - /// instead the program must wait until the Main Status Register signalizes that the command has been completed, and then it must (!) send a - /// Sense Interrupt State command to 'terminate' the Seek/Recalibrate command. - /// - Result - } - - /// - /// The lifecycle of an instruction - /// Similar to phase, this describes the current 'sub-phase' we are in when dealing with an instruction - /// - private enum InstructionState - { - /// - /// FDC has received a command byte and is currently reading parameter bytes from the data bus - /// - ReceivingParameters, - - /// - /// All parameter bytes have been received. This phase allows any neccessary setup before instruction execution starts - /// - PreExecution, - - /// - /// The start of instruction execution. This may end up with the FDC moving into result phase, - /// but also may also prepare the way for further processing to occur later in execution phase - /// - StartExecute, - - /// - /// Data is read or written in execution phase - /// - ExecutionReadWrite, - - /// - /// Execution phase is well under way. This state primarily deals with data transfer between CPU and FDC - /// - ExecutionWrite, - - /// - /// Execution phase is well under way. This state primarily deals with data transfer between FDC and CPU - /// - ExecutionRead, - - /// - /// Execution has finished and results bytes are ready to be read by the CPU - /// Initial result setup - /// - StartResult, - - /// - /// Result processing - /// - ProcessResult, - - /// - /// Results are being sent - /// - SendingResults, - - /// - /// Final cleanup tasks when the instruction has fully completed - /// - Completed - } - - /// - /// Represents internal interrupt state of the FDC - /// - public enum InterruptState - { - /// - /// There is no interrupt - /// - None, - /// - /// Execution interrupt - /// - Execution, - /// - /// Result interrupt - /// - Result, - /// - /// Ready interrupt - /// - Ready, - /// - /// Seek interrupt - /// - Seek - } - - /// - /// Possible main states that each drive can be in - /// - public enum DriveMainState - { - /// - /// Drive is not doing anything - /// - None, - /// - /// Seek operation is in progress - /// - Seek, - /// - /// Recalibrate operation is in progress - /// - Recalibrate, - /// - /// A scan data operation is in progress - /// - Scan, - /// - /// A read ID operation is in progress - /// - ReadID, - /// - /// A read data operation is in progress - /// - ReadData, - /// - /// A read diagnostic (read track) operation is in progress - /// - ReadDiagnostic, - /// - /// A write id (format track) operation is in progress - /// - WriteID, - /// - /// A write data operation is in progress - /// - WriteData, - } - - /// - /// State information during a seek/recalibration operation - /// - public enum SeekSubState - { - /// - /// Seek hasnt started yet - /// - Idle, - /// - /// Delayed - /// - Wait, - /// - /// Setup for head move - /// - MoveInit, - /// - /// Seek is currently happening - /// - HeadMove, - /// - /// Head move with no delay - /// - MoveImmediate, - /// - /// Ready to complete - /// - PerformCompletion, - /// - /// Seek operation has completed - /// - SeekCompleted - } - - /// - /// Seek int code - /// - public enum SeekIntStatus - { - Normal, - Abnormal, - DriveNotReady, - } - - /// - /// The direction of a specific command - /// - private enum CommandDirection - { - /// - /// Data flows from UPD765A to Z80 - /// - OUT, - /// - /// Data flows from Z80 to UPD765A - /// - IN - } - - /// - /// Enum defining the different types of result that can be returned - /// - private enum ResultType - { - /// - /// Standard 7 result bytes are returned - /// - Standard, - /// - /// 1 byte returned - ST3 - /// (used for SenseDriveStatus) - /// - ST3, - /// - /// 1 byte returned - ST0 - /// (used for version & invalid) - /// - ST0, - /// - /// 2 bytes returned for sense interrupt status command - /// ST0 - /// CurrentCylinder - /// - Interrupt - } - - /// - /// Possible list of encountered drive status errors - /// - public enum Status - { - /// - /// No error detected - /// - None, - /// - /// An undefined error has been detected - /// - Undefined, - /// - /// Drive is not ready - /// - DriveNotReady, - /// - /// Invalid command received - /// - Invalid, - /// - /// The disk has its write protection tab enabled - /// - WriteProtected, - /// - /// The requested sector has not been found - /// - SectorNotFound - } - - /// - /// Represents the direction that the head is moving over the cylinders - /// Increment: Track number increasing (head moving from outside of disk inwards) - /// Decrement: Track number decreasing (head moving from inside of disk outwards) - /// - public enum SkipDirection - { - Increment, - Decrement - } - - // Command Instruction Constants - // Designates the default postitions within the cmdbuffer array - - public const int CM_HEAD = 0; - /// - /// C - Track - /// - public const int CM_C = 1; - /// - /// H - Side - /// - public const int CM_H = 2; - /// - /// R - Sector ID - /// - public const int CM_R = 3; - /// - /// N - Sector size - /// - public const int CM_N = 4; - /// - /// EOT - End of track - /// - public const int CM_EOT = 5; - /// - /// GPL - Gap length - /// - public const int CM_GPL = 6; - /// - /// DTL - Data length - /// - public const int CM_DTL = 7; - /// - /// STP - Step - /// - public const int CM_STP = 7; - - // Result Instruction Constants - // Designates the default postitions within the cmdbuffer array - - /// - /// Status register 0 - /// - public const int RS_ST0 = 0; - /// - /// Status register 1 - /// - public const int RS_ST1 = 1; - /// - /// Status register 2 - /// - public const int RS_ST2 = 2; - /// - /// C - Track - /// - public const int RS_C = 3; - /// - /// H - Side - /// - public const int RS_H = 4; - /// - /// R - Sector ID - /// - public const int RS_R = 5; - /// - /// N - Sector size - /// - public const int RS_N = 6; - - // Main Status Register Constants - // Designates the bit positions within the Main status register - - /// - /// FDD0 Busy (seek/recalib active, until succesful sense intstat) - /// FDD number 0 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. - /// - public const int MSR_D0B = 0; - /// - /// FDD1 Busy (seek/recalib active, until succesful sense intstat) - /// FDD number 1 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. - /// - public const int MSR_D1B = 1; - /// - /// FDD2 Busy (seek/recalib active, until succesful sense intstat) - /// FDD number 2 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. - /// - public const int MSR_D2B = 2; - /// - /// FDD3 Busy (seek/recalib active, until succesful sense intstat) - /// FDD number 3 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. - /// - public const int MSR_D3B = 3; - /// - /// FDC Busy (still in command-, execution- or result-phase) - /// A Read or Write command is in orocess. (FDC Busy) FDC will not accept any other command - /// - public const int MSR_CB = 4; - /// - /// Execution Mode (still in execution-phase, non_DMA_only) - /// This bit is set only during execution ohase (Execution Mode) in non-DMA mode When DB5 goes low, execution phase has ended and result phase has started.It operates only during - /// non-DMA mode of operation - /// - public const int MSR_EXM = 5; - /// - /// Data Input/Output (0=CPU->FDC, 1=FDC->CPU) (see b7) - /// Indicates direction of data transfer between FDC and data regrster If DIO = 1, then transfer is from data register to the - /// processor.If DIO = 0, then transfer is from the processor to data register - /// - public const int MSR_DIO = 6; - /// - /// Request For Master (1=ready for next byte) (see b6 for direction) - /// ndicates data register IS ready to send or receive data to or from the processor Both bits DIO and RQM should be - /// used to perform the hand-shaking functions of “ready” and “directron” to the processor - /// - public const int MSR_RQM = 7; - - // Status Register 0 Constants - // Designates the bit positions within the status register 0 - - /// - /// Unit Select (driveno during interrupt) - /// This flag IS used to indicate a drive unit number at interrupt - /// - public const int SR0_US0 = 0; - - /// - /// Unit Select (driveno during interrupt) - /// This flag IS used to indicate a drive unit number at interrupt - /// - public const int SR0_US1 = 1; - - /// - /// Head Address (head during interrupt) - /// State of the head at interrupt - /// - public const int SR0_HD = 2; - - /// - /// Not Ready (drive not ready or non-existing 2nd head selected) - /// Not Ready - When the FDD IS in the not-ready state and a Read or Write command IS Issued, this - /// flag IS set If a Read or Write command is issued to side 1 of a single-sided drive, then this flag IS set - /// - public const int SR0_NR = 3; - - /// - /// Equipment Check (drive failure or recalibrate failed (retry)) - /// Equipment check - If a fault srgnal IS received from the FDD, or if the track 0 srgnal fails to occur after 77 - /// step pulses(Recalibrate Command) then this flag is set - /// - public const int SR0_EC = 4; - - /// - /// Seek End (Set if seek-command completed) - /// Seek end - When the FDC completes the Seek command, this flag IS set lo 1 (high) - /// - public const int SR0_SE = 5; - - /// - /// Interrupt Code (low byte) - /// Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd - /// or senseint with no int occured, 3=aborted:disc removed etc.) - /// - public const int SR0_IC0 = 6; - - /// - /// Interrupt Code (high byte) - /// Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd - /// or senseint with no int occured, 3=aborted:disc removed etc.) - /// - public const int SR0_IC1 = 7; - - // Status Register 1 Constants - // Designates the bit positions within the status register 1 - - /// - /// Missing Address Mark (Sector_ID or DAM not found) - /// Missing address mark - This bit is set i f the FDC does not detect the IDAM before 2 index pulses It is also set if - /// the FDC cannot find the DAM or DDAM after the IDAM i s found.MD bit of ST2 is also set at this time - /// - public const int SR1_MA = 0; - - /// - /// Not Writeable (tried to write/format disc with wprot_tab=on) - /// Not writable (write protect) - During execution of Write Data, Write Deleted Data or Write ID command. if the FDC - /// detect: a write protect srgnal from the FDD.then this flag is Set - /// - public const int SR1_NW = 1; - - /// - /// No Data - /// No Data (Sector_ID not found, CRC fail in ID_field) - /// - /// During execution of Read Data. Read Deleted Data Write Data.Write Deleted Data or Scan command, if the FDC cannot find - /// the sector specified in the IDR(2)Register, this flag i s set. - /// - /// During execution of the Read ID command. if the FDC cannot read the ID field without an error, then this flag IS set - /// - /// During execution of the Read Diagnostic command. if the starting sector cannot be found, then this flag is set - /// - public const int SR1_ND = 2; - - /// - /// Over Run (CPU too slow in execution-phase (ca. 26us/Byte)) - /// Overrun - If the FDC i s not serviced by the host system during data transfers within a certain time interval.this flaa i s set - /// - public const int SR1_OR = 4; - - /// - /// Data Error (CRC-fail in ID- or Data-Field) - /// Data error - When the FDC detects a CRC(1) error in either the ID field or the data field, this flag is set - /// - public const int SR1_DE = 5; - - /// - /// End of Track (set past most read/write commands) (see IC) - /// End of cylinder - When the FDC tries to access a sector beyond the final sector of a cylinder, this flag I S set - /// - public const int SR1_EN = 7; - - // Status Register 2 Constants - // Designates the bit positions within the status register 2 - - /// - /// Missing Address Mark in Data Field (DAM not found) - /// Missing address mark - When data IS read from the medium, i f the FDC cannot find a data address mark or deleted - /// data address mark, then this flag is set - /// - public const int SR2_MD = 0; - - /// - /// Bad Cylinder (read/programmed track-ID different and read-ID = FF) - /// Bad cylinder - This bit i s related to the ND bit. and when the contents of C on the medium is different - /// from that stored i n the IDR and the contents of C IS FFH.then this flag IS set - /// - public const int SR2_BC = 1; - - /// - /// Scan Not Satisfied (no fitting sector found) - /// Scan not satisfied - During execution of the Scan command, i f the F D cannot find a sector on the cylinder - /// which meets the condition.then this flag i s set - /// - public const int SR2_SN = 2; - - /// - /// Scan Equal Hit (equal) - /// Scan equal hit - During execution of the Scan command. i f the condition of “equal” is satisfied, this flag i s set - /// - public const int SR2_SH = 3; - - /// - /// Wrong Cylinder (read/programmed track-ID different) (see b1) - /// Wrong cylinder - This bit IS related to the ND bit, and when the contents of C(3) on the medium is different - /// from that stored i n the IDR.this flag is set - /// - public const int SR2_WC = 4; - - /// - /// Data Error in Data Field (CRC-fail in data-field) - /// Data error in data field - If the FDC detects a CRC error i n the data field then this flag is set - /// - public const int SR2_DD = 5; - - /// - /// Control Mark (read/scan command found sector with deleted DAM) - /// Control mark - During execution of the Read Data or Scan command, if the FDC encounters a sector - /// which contains a deleted data address mark, this flag is set Also set if DAM is - /// found during Read Deleted Data - /// - public const int SR2_CM = 6; - - // Status Register 3 Constants - // Designates the bit positions within the status register 3 - - /// - /// Unit select 0 - /// Unit Select (pin 28,29 of FDC) - /// - public const int SR3_US0 = 0; - - /// - /// Unit select 1 - /// Unit Select (pin 28,29 of FDC) - /// - public const int SR3_US1 = 1; - - /// - /// Head address (side select) - /// Head Address (pin 27 of FDC) - /// - public const int SR3_HD = 2; - - /// - /// Two Side (0=yes, 1=no (!)) - /// Two-side - This bit IS used to indicate the status of the two-side signal from the FDD - /// - public const int SR3_TS = 3; - - /// - /// Track 0 (on track 0 we are) - /// Track 0 - This bit IS used to indicate the status of the track 0 signal from the FDD - /// - public const int SR3_T0 = 4; - - /// - /// Ready - status of the ready signal from the fdd - /// Ready (drive ready signal) - /// - public const int SR3_RY = 5; - - /// - /// Write Protected (write protected) - /// Write protect - status of the wp signal from the fdd - /// - public const int SR3_WP = 6; - - /// - /// Fault - This bit is used to indicate the status of the fault signal from the FDD - /// Fault (if supported: 1=Drive failure) - /// - public const int SR3_FT = 7; - - // Interrupt Code Masks - - /// - /// 1 = aborted:readfail / OK if EN (end of track) - /// - public const byte IC_OK = 0x00; - - /// - /// 1 = aborted:readfail / OK if EN (end of track) - /// - public const byte IC_ABORTED_RF_OKEN = 0x40; - - /// - /// 2 = unknown cmd or senseint with no int occured - /// - public const byte IC_NO_INT_OCCURED = 0x80; - - /// - /// 3 = aborted:disc removed etc - /// - public const byte IC_ABORTED_DISCREMOVED = 0xC0; - - // command code constants - public const int CC_READ_DATA = 0x06; - public const int CC_READ_ID = 0x0a; - public const int CC_SPECIFY = 0x03; - public const int CC_READ_DIAGNOSTIC = 0x02; - public const int CC_SCAN_EQUAL = 0x11; - public const int CC_SCAN_HIGHOREQUAL = 0x1d; - public const int CC_SCAN_LOWOREQUAL = 0x19; - public const int CC_READ_DELETEDDATA = 0x0c; - public const int CC_WRITE_DATA = 0x05; - public const int CC_WRITE_ID = 0x0d; - public const int CC_WRITE_DELETEDDATA = 0x09; - public const int CC_SEEK = 0x0f; - public const int CC_RECALIBRATE = 0x07; - public const int CC_SENSE_INTSTATUS = 0x08; - public const int CC_SENSE_DRIVESTATUS = 0x04; - public const int CC_VERSION = 0x10; - public const int CC_INVALID = 0x00; - - // drive seek state constants - public const int SEEK_IDLE = 0; - public const int SEEK_SEEK = 1; - public const int SEEK_RECALIBRATE = 2; - // seek interrupt - public const int SEEK_INTACKNOWLEDGED = 3; - public const int SEEK_NORMALTERM = 4; - public const int SEEK_ABNORMALTERM = 5; - public const int SEEK_DRIVENOTREADY = 6; - - /// - /// Class that holds information about a specific command - /// - private class Command - { -// /// -// /// Mask to remove potential parameter bits (5,6, and or 7) in order to identify the command -// /// -// public int BitMask { get; set; } - /// - /// The command code after bitmask has been applied - /// - public int CommandCode { get; set; } - /// - /// The number of bytes that make up the full command - /// - public int ParameterByteCount { get; set; } - /// - /// The number of result bytes that will be generated from the command - /// - public int ResultByteCount { get; set; } - /// - /// The command direction - /// IN - Z80 to UPD765A - /// OUT - UPD765A to Z80 - /// - public CommandDirection Direction { get; set; } - /// - /// Command makes use of the MT bit - /// - public bool MT; - /// - /// Command makes use of the MF bit - /// - public bool MF; - /// - /// Command makes use of the SK bit - /// - public bool SK; - /// - /// Read/Write command that is READ - /// - public bool IsRead; - /// - /// Read/Write command that is WRITE - /// - public bool IsWrite; - - /// - /// Delegate function that is called by this command - /// bool 1: EXECUTE - if TRUE the command will be executed. if FALSE the method will instead parse commmand parameter bytes - /// bool 2: RESULT - if TRUE - /// - public Action CommandDelegate { get; set; } - } - - /// - /// Storage for command parameters - /// - public class CommandParameters - { - /// - /// The requested drive - /// - public byte UnitSelect; - /// - /// The requested physical side - /// - public byte Side; - /// - /// The requested track (C) - /// - public byte Cylinder; - /// - /// The requested head (H) - /// - public byte Head; - /// - /// The requested sector (R) - /// - public byte Sector; - /// - /// The specified sector size (N) - /// - public byte SectorSize; - /// - /// The end of track or last sector value (EOT) - /// - public byte EOT; - /// - /// Gap3 length (GPL) - /// - public byte Gap3Length; - /// - /// Data length (DTL) - When N is defined as 00, DTL stands for the data length - /// which users are going to read out or write into the sector - /// - public byte DTL; - - /// - /// Clear down - /// - public void Reset() - { - UnitSelect = 0; - Side = 0; - Cylinder = 0; - Head = 0; - Sector = 0; - SectorSize = 0; - EOT = 0; - Gap3Length = 0; - DTL = 0; - } - - public void SyncState(Serializer ser) - { - ser.BeginSection("ActiveCmdParams"); - - ser.Sync(nameof(UnitSelect), ref UnitSelect); - ser.Sync(nameof(Side), ref Side); - ser.Sync(nameof(Cylinder), ref Cylinder); - ser.Sync(nameof(Head), ref Head); - ser.Sync(nameof(Sector), ref Sector); - ser.Sync(nameof(SectorSize), ref SectorSize); - ser.Sync(nameof(EOT), ref EOT); - ser.Sync(nameof(Gap3Length), ref Gap3Length); - ser.Sync(nameof(DTL), ref DTL); - - ser.EndSection(); - } - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.FDC.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.FDC.cs deleted file mode 100644 index 04df7579e46..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.FDC.cs +++ /dev/null @@ -1,2813 +0,0 @@ -using BizHawk.Common.NumberExtensions; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// FDC State and Methods - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 - { - /// - /// Signs whether the drive is active - /// - public bool DriveLight; - - /// - /// Collection of possible commands - /// - private List CommandList; - - /// - /// State parameters relating to the Active command - /// - public CommandParameters ActiveCommandParams = new CommandParameters(); - - /// - /// The current active phase of the controller - /// - private Phase ActivePhase = Phase.Command; - - /// - /// The currently active interrupt - /// - private InterruptState ActiveInterrupt = InterruptState.None; - /// - /// Command buffer - /// This does not contain the initial command byte (only parameter bytes) - /// - private byte[] CommBuffer = new byte[9]; - - /// - /// Current index within the command buffer - /// - private int CommCounter = 0; - - /// - /// Initial command byte flag - /// Bit7 Multi Track (continue multi-sector-function on other head) - /// - private bool CMD_FLAG_MT; - - /// - /// Initial command byte flag - /// Bit6 MFM-Mode-Bit (Default 1=Double Density) - /// - private bool CMD_FLAG_MF; - - /// - /// Initial command byte flag - /// Bit5 Skip-Bit (set if secs with deleted DAM shall be skipped) - /// - private bool CMD_FLAG_SK; - - /// - /// Step Rate Time (supplied via the specify command) - /// SRT stands for the steooino rate for the FDD ( 1 to 16 ms in 1 ms increments). - /// Stepping rate applies to all drives(FH= 1ms, EH= 2ms, etc.). - /// - private int SRT; - - /// - /// Keeps track of the current SRT state - /// - private int SRT_Counter; - - /// - /// Head Unload Time (supplied via the specify command) - /// HUT stands for the head unload time after a Read or Write operation has occurred - /// (16 to 240 ms in 16 ms Increments) - /// - private int HUT; - - /// - /// Keeps track of the current HUT state - /// - private int HUT_Counter; - - /// - /// Head load Time (supplied via the specify command) - /// HLT stands for the head load time in the FDD (2 to 254 ms in 2 ms Increments) - /// - private int HLT; - - /// - /// Keeps track of the current HLT state - /// - private int HLT_Counter; - - /// - /// Non-DMA Mode (supplied via the specify command) - /// ND stands for operation in the non-DMA mode - /// - private bool ND; - - /// - /// In lieu of actual timing, this will count status reads in execution phase - /// where the CPU hasnt actually read any bytes - /// - private int OverrunCounter; - - /// - /// Contains result bytes in result phase - /// - private byte[] ResBuffer = new byte[7]; - - /// - /// Contains sector data to be written/read in execution phase - /// - private byte[] ExecBuffer = new byte[0x8000]; - - /// - /// Interrupt result buffer - /// Persists (and returns when needed) the last result data when a sense interrupt status command happens - /// - private byte[] InterruptResultBuffer = new byte[2]; - - /// - /// Current index within the result buffer - /// - private int ResCounter = 0; - - /// - /// The byte length of the currently active command - /// This may or may not be the same as the actual command resultbytes value - /// - private int ResLength = 0; - - /// - /// Index for sector data within the result buffer - /// - private int ExecCounter = 0; - - /// - /// The length of the current exec command - /// - private int ExecLength = 0; - - /// - /// The last write byte that was received during execution phase - /// - private byte LastSectorDataWriteByte = 0; - - /// - /// The last read byte to be sent during execution phase - /// - private byte LastSectorDataReadByte = 0; - - /// - /// The last parameter byte that was written to the FDC - /// - private byte LastByteReceived = 0; - - /// - /// Delay for reading sector - /// - private int SectorDelayCounter = 0; - - /// - /// The phyical sector ID - /// - private int SectorID = 0; - - /// - /// Counter for index pulses - /// - private int IndexPulseCounter; - - /// - /// Specifies the index of the currently selected command (in the CommandList) - /// - public int CMDIndex - { - get => _cmdIndex; - set - { - _cmdIndex = value; - ActiveCommand = CommandList[_cmdIndex]; - } - } - private int _cmdIndex; - - /// - /// The currently active command - /// - private Command ActiveCommand; - - /// - /// Main status register (accessed via reads to port 0x2ffd) - /// - /* - b0..3 DB FDD0..3 Busy (seek/recalib active, until succesful sense intstat) - b4 CB FDC Busy (still in command-, execution- or result-phase) - b5 EXM Execution Mode (still in execution-phase, non_DMA_only) - b6 DIO Data Input/Output (0=CPU->FDC, 1=FDC->CPU) (see b7) - b7 RQM Request For Master (1=ready for next byte) (see b6 for direction) - */ - private byte StatusMain; - - /// - /// Status Register 0 - /// - /* - b0,1 US Unit Select (driveno during interrupt) - b2 HD Head Address (head during interrupt) - b3 NR Not Ready (drive not ready or non-existing 2nd head selected) - b4 EC Equipment Check (drive failure or recalibrate failed (retry)) - b5 SE Seek End (Set if seek-command completed) - b6,7 IC Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd - or senseint with no int occured, 3=aborted:disc removed etc.) - */ - private byte Status0; - - /// - /// Status Register 1 - /// - /* - b0 MA Missing Address Mark (Sector_ID or DAM not found) - b1 NW Not Writeable (tried to write/format disc with wprot_tab=on) - b2 ND No Data (Sector_ID not found, CRC fail in ID_field) - b3,6 0 Not used - b4 OR Over Run (CPU too slow in execution-phase (ca. 26us/Byte)) - b5 DE Data Error (CRC-fail in ID- or Data-Field) - b7 EN End of Track (set past most read/write commands) (see IC) - */ - private byte Status1; - - /// - /// Status Register 2 - /// - /* - b0 MD Missing Address Mark in Data Field (DAM not found) - b1 BC Bad Cylinder (read/programmed track-ID different and read-ID = FF) - b2 SN Scan Not Satisfied (no fitting sector found) - b3 SH Scan Equal Hit (equal) - b4 WC Wrong Cylinder (read/programmed track-ID different) (see b1) - b5 DD Data Error in Data Field (CRC-fail in data-field) - b6 CM Control Mark (read/scan command found sector with deleted DAM) - b7 0 Not Used - */ - private byte Status2; - - /// - /// Status Register 3 - /// - /* - b0,1 US Unit Select (pin 28,29 of FDC) - b2 HD Head Address (pin 27 of FDC) - b3 TS Two Side (0=yes, 1=no (!)) - b4 T0 Track 0 (on track 0 we are) - b5 RY Ready (drive ready signal) - b6 WP Write Protected (write protected) - b7 FT Fault (if supported: 1=Drive failure) - */ - private byte Status3; - - /// - /// Read Data - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data transfer between FDD and FDC - /// RESULT: 7 result bytes - /// - private void UPD_ReadData() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - setup for execution phase - - // clear exec buffer and status registers - ClearExecBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // temp sector index - byte secIdx = ActiveCommandParams.Sector; - - // hack for when another drive (non-existent) is being called - if (ActiveDrive.ID != 0) - DiskDriveIndex = 0; - - // do we have a valid disk inserted? - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - int buffPos = 0; - int sectorSize = 0; // why doesn't this get flagged for CS0219? -#pragma warning disable CS0219 - int maxTransferCap = 0; -#pragma warning restore CS0219 - - // calculate requested size of data required - if (ActiveCommandParams.SectorSize == 0) - { - // When N=0, then DTL defines the data length which the FDC must treat as a sector. If DTL is smaller than the actual - // data length in a sector, the data beyond DTL in the sector is not sent to the Data Bus. The FDC reads (internally) - // the complete sector performing the CRC check and, depending upon the manner of command termination, may perform - // a Multi-Sector Read Operation. - sectorSize = ActiveCommandParams.DTL; - - // calculate maximum transfer capacity - if (!CMD_FLAG_MF) - maxTransferCap = 3328; - -// if (maxTransferCap is 0) { } - } - else - { - // When N is non - zero, then DTL has no meaning and should be set to ffh - ActiveCommandParams.DTL = 0xFF; - - // calculate maximum transfer capacity - switch (ActiveCommandParams.SectorSize) - { - case 1: - if (CMD_FLAG_MF) - maxTransferCap = 6656; - else - maxTransferCap = 3840; - break; - case 2: - if (CMD_FLAG_MF) - maxTransferCap = 7680; - else - maxTransferCap = 4096; - break; - case 3: - if (CMD_FLAG_MF) - maxTransferCap = 8192; - else - maxTransferCap = 4096; - break; - } - - sectorSize = 0x80 << ActiveCommandParams.SectorSize; - } - - // get the current track - var track = ActiveDrive.Disk.DiskTracks.FirstOrDefault(a => a.TrackNumber == ActiveDrive.CurrentTrackID); - - if (track == null || track.NumberOfSectors <= 0) - { - // track could not be found - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - FloppyDisk.Sector sector = null; - - // sector read loop - for (; ; ) - { - bool terminate = false; - - // lookup the sector - sector = GetSector(); - - if (sector == null) - { - // sector was not found after two passes of the disk index hole - SetBit(SR1_ND, ref Status1); - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - - // result requires the actual track id, rather than the sector track id - ActiveCommandParams.Cylinder = track.TrackNumber; - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Result; - break; - } - - // sector ID was found on this track - - // get status regs from sector - Status1 = sector.Status1; - Status2 = sector.Status2; - - // we don't need EN - UnSetBit(SR1_EN, ref Status1); - - // If SK=1, the FDC skips the sector with the Deleted Data Address Mark and reads the next sector. - // The CRC bits in the deleted data field are not checked when SK=1 - if (CMD_FLAG_SK && Status2.Bit(SR2_CM)) - { - if (ActiveCommandParams.Sector != ActiveCommandParams.EOT) - { - // increment the sector ID and search again - ActiveCommandParams.Sector++; - continue; - } - else - { - // no execution phase - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - - // result requires the actual track id, rather than the sector track id - ActiveCommandParams.Cylinder = track.TrackNumber; - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Result; - break; - } - } - - // read the sector - for (int i = 0; i < sector.DataLen; i++) - { - ExecBuffer[buffPos++] = sector.ActualData[i]; - } - - // mark the sector read - sector.SectorReadCompleted(); - - // any CRC errors? - if (Status1.Bit(SR1_DE) || Status2.Bit(SR2_DD)) - { - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - terminate = true; - } - - if (!CMD_FLAG_SK && Status2.Bit(SR2_CM)) - { - // deleted address mark was detected with NO skip flag set - ActiveCommandParams.EOT = ActiveCommandParams.Sector; - SetBit(SR2_CM, ref Status2); - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - terminate = true; - } - - if (sector.SectorID == ActiveCommandParams.EOT || terminate) - { - // this was the last sector to read - // or termination requested - - SetBit(SR1_EN, ref Status1); - - int keyIndex = 0; - for (int i = 0; i < track.Sectors.Length; i++) - { - if (track.Sectors[i].SectorID == sector.SectorID) - { - keyIndex = i; - break; - } - } - - if (keyIndex == track.Sectors.Length - 1) - { - // last sector on the cylinder, set EN - SetBit(SR1_EN, ref Status1); - - // increment cylinder - ActiveCommandParams.Cylinder++; - - // reset sector - ActiveCommandParams.Sector = sector.SectorID; // 1; - ActiveDrive.SectorIndex = 0; - } - else - { - ActiveDrive.SectorIndex++; - } - - UnSetBit(SR0_IC1, ref Status0); - if (terminate) - SetBit(SR0_IC0, ref Status0); - else - UnSetBit(SR0_IC0, ref Status0); - - SetBit(SR0_IC0, ref Status0); - - // result requires the actual track id, rather than the sector track id - ActiveCommandParams.Cylinder = track.TrackNumber; - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Execution; - break; - } - else - { - // continue with multi-sector read operation - ActiveCommandParams.Sector++; - //ActiveDrive.SectorIndex++; - } - } - - if (ActivePhase == Phase.Execution) - { - ExecLength = buffPos; - ExecCounter = buffPos; - - DriveLight = true; - } - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - - var index = ExecLength - ExecCounter; - - LastSectorDataReadByte = ExecBuffer[index]; - - OverrunCounter--; - ExecCounter--; - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Read Deleted Data - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data transfer between the FDD and FDC - /// RESULT: 7 result bytes - /// - private void UPD_ReadDeletedData() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - setup for execution phase - - // clear exec buffer and status registers - ClearExecBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // temp sector index - byte secIdx = ActiveCommandParams.Sector; - - // do we have a valid disk inserted? - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - int buffPos = 0; - int sectorSize = 0; -#pragma warning disable CS0219 - int maxTransferCap = 0; -// if (maxTransferCap > 0) { } -#pragma warning restore CS0219 - - // calculate requested size of data required - if (ActiveCommandParams.SectorSize == 0) - { - // When N=0, then DTL defines the data length which the FDC must treat as a sector. If DTL is smaller than the actual - // data length in a sector, the data beyond DTL in the sector is not sent to the Data Bus. The FDC reads (internally) - // the complete sector performing the CRC check and, depending upon the manner of command termination, may perform - // a Multi-Sector Read Operation. - sectorSize = ActiveCommandParams.DTL; - - // calculate maximum transfer capacity - if (!CMD_FLAG_MF) - maxTransferCap = 3328; - } - else - { - // When N is non - zero, then DTL has no meaning and should be set to ffh - ActiveCommandParams.DTL = 0xFF; - - // calculate maximum transfer capacity - switch (ActiveCommandParams.SectorSize) - { - case 1: - if (CMD_FLAG_MF) - maxTransferCap = 6656; - else - maxTransferCap = 3840; - break; - case 2: - if (CMD_FLAG_MF) - maxTransferCap = 7680; - else - maxTransferCap = 4096; - break; - case 3: - if (CMD_FLAG_MF) - maxTransferCap = 8192; - else - maxTransferCap = 4096; - break; - } - - sectorSize = 0x80 << ActiveCommandParams.SectorSize; - } - - // get the current track - var track = ActiveDrive.Disk.DiskTracks.FirstOrDefault(a => a.TrackNumber == ActiveDrive.CurrentTrackID); - - if (track == null || track.NumberOfSectors <= 0) - { - // track could not be found - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - FloppyDisk.Sector sector = null; - - // sector read loop - for (; ; ) - { - bool terminate = false; - - // lookup the sector - sector = GetSector(); - - if (sector == null) - { - // sector was not found after two passes of the disk index hole - SetBit(SR1_ND, ref Status1); - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - - // result requires the actual track id, rather than the sector track id - ActiveCommandParams.Cylinder = track.TrackNumber; - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Result; - break; - } - - // sector ID was found on this track - - // get status regs from sector - Status1 = sector.Status1; - Status2 = sector.Status2; - - // we don't need EN - UnSetBit(SR1_EN, ref Status1); - - // invert CM for read deleted data command - if (Status2.Bit(SR2_CM)) - UnSetBit(SR2_CM, ref Status2); - else - SetBit(SR2_CM, ref Status2); - - // skip flag is set and no DAM found - if (CMD_FLAG_SK && Status2.Bit(SR2_CM)) - { - if (ActiveCommandParams.Sector != ActiveCommandParams.EOT) - { - // increment the sector ID and search again - ActiveCommandParams.Sector++; - continue; - } - else - { - // no execution phase - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - - // result requires the actual track id, rather than the sector track id - ActiveCommandParams.Cylinder = track.TrackNumber; - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Result; - break; - } - } - // we can read this sector - else - { - // if DAM is not set this will be the last sector to read - if (Status2.Bit(SR2_CM)) - { - ActiveCommandParams.EOT = ActiveCommandParams.Sector; - } - - if (!CMD_FLAG_SK && !Status2.Bit(SR2_CM) - && ActiveDrive.Disk.Protection is ProtectionType.PaulOwens) - { - ActiveCommandParams.EOT = ActiveCommandParams.Sector; - SetBit(SR2_CM, ref Status2); - SetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_IC1, ref Status0); - terminate = true; - } - - // read the sector - for (int i = 0; i < sectorSize; i++) - { - ExecBuffer[buffPos++] = sector.ActualData[i]; - } - - // mark the sector read - sector.SectorReadCompleted(); - - if (sector.SectorID == ActiveCommandParams.EOT) - { - // this was the last sector to read - - SetBit(SR1_EN, ref Status1); - - int keyIndex = 0; - for (int i = 0; i < track.Sectors.Length; i++) - { - if (track.Sectors[i].SectorID == sector.SectorID) - { - keyIndex = i; - break; - } - } - - if (keyIndex == track.Sectors.Length - 1) - { - // last sector on the cylinder, set EN - SetBit(SR1_EN, ref Status1); - - // increment cylinder - ActiveCommandParams.Cylinder++; - - // reset sector - ActiveCommandParams.Sector = 1; - ActiveDrive.SectorIndex = 0; - } - else - { - ActiveDrive.SectorIndex++; - } - - UnSetBit(SR0_IC1, ref Status0); - if (terminate) - SetBit(SR0_IC0, ref Status0); - else - UnSetBit(SR0_IC0, ref Status0); - - SetBit(SR0_IC0, ref Status0); - - // result requires the actual track id, rather than the sector track id - ActiveCommandParams.Cylinder = track.TrackNumber; - - // remove CM (appears to be required to defeat Alkatraz copy protection) - UnSetBit(SR2_CM, ref Status2); - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Execution; - break; - } - else - { - // continue with multi-sector read operation - ActiveCommandParams.Sector++; - //ActiveDrive.SectorIndex++; - } - } - } - - if (ActivePhase == Phase.Execution) - { - ExecLength = buffPos; - ExecCounter = buffPos; - DriveLight = true; - } - } - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - var index = ExecLength - ExecCounter; - - LastSectorDataReadByte = ExecBuffer[index]; - - OverrunCounter--; - ExecCounter--; - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Read Diagnostic (read track) - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data transfer between FDD and FDC. FDC reads all data fields from index hole to EDT - /// RESULT: 7 result bytes - /// - private void UPD_ReadDiagnostic() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - setup for execution phase - - // clear exec buffer and status registers - ClearExecBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // temp sector index - byte secIdx = ActiveCommandParams.Sector; - - // do we have a valid disk inserted? - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - int buffPos = 0; - int sectorSize = 0; -#pragma warning disable CS0219 - int maxTransferCap = 0; -// if (maxTransferCap > 0) { } -#pragma warning restore CS0219 - - // calculate requested size of data required - if (ActiveCommandParams.SectorSize == 0) - { - // When N=0, then DTL defines the data length which the FDC must treat as a sector. If DTL is smaller than the actual - // data length in a sector, the data beyond DTL in the sector is not sent to the Data Bus. The FDC reads (internally) - // the complete sector performing the CRC check and, depending upon the manner of command termination, may perform - // a Multi-Sector Read Operation. - sectorSize = ActiveCommandParams.DTL; - - // calculate maximum transfer capacity - if (!CMD_FLAG_MF) - maxTransferCap = 3328; - } - else - { - // When N is non - zero, then DTL has no meaning and should be set to ffh - ActiveCommandParams.DTL = 0xFF; - - // calculate maximum transfer capacity - switch (ActiveCommandParams.SectorSize) - { - case 1: - if (CMD_FLAG_MF) - maxTransferCap = 6656; - else - maxTransferCap = 3840; - break; - case 2: - if (CMD_FLAG_MF) - maxTransferCap = 7680; - else - maxTransferCap = 4096; - break; - case 3: - if (CMD_FLAG_MF) - maxTransferCap = 8192; - else - maxTransferCap = 4096; - break; - } - - sectorSize = 0x80 << ActiveCommandParams.SectorSize; - } - - // get the current track - var track = ActiveDrive.Disk.DiskTracks.FirstOrDefault(a => a.TrackNumber == ActiveDrive.CurrentTrackID); - - if (track == null || track.NumberOfSectors <= 0) - { - // track could not be found - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - //FloppyDisk.Sector sector = null; - ActiveDrive.SectorIndex = 0; - - int secCount = 0; - - // read the whole track - for (int i = 0; i < track.Sectors.Length; i++) - { - if (secCount >= ActiveCommandParams.EOT) - { - break; - } - - var sec = track.Sectors[i]; - for (int b = 0; b < sec.ActualData.Length; b++) - { - ExecBuffer[buffPos++] = sec.ActualData[b]; - } - - // mark the sector read - sec.SectorReadCompleted(); - - // end of sector - compare IDs - if (sec.TrackNumber != ActiveCommandParams.Cylinder - || sec.SideNumber != ActiveCommandParams.Head - || sec.SectorID != ActiveCommandParams.Sector - || sec.SectorSize != ActiveCommandParams.SectorSize) - { - SetBit(SR1_ND, ref Status1); - } - - secCount++; - ActiveDrive.SectorIndex = i; - } - - if (secCount == ActiveCommandParams.EOT) - { - // this was the last sector to read - // or termination requested - - int keyIndex = 0; - for (int i = 0; i < track.Sectors.Length; i++) - { - if (track.Sectors[i].SectorID == track.Sectors[ActiveDrive.SectorIndex].SectorID) - { - keyIndex = i; - break; - } - } - - if (keyIndex == track.Sectors.Length - 1) - { - // last sector on the cylinder, set EN - SetBit(SR1_EN, ref Status1); - - // increment cylinder - ActiveCommandParams.Cylinder++; - - // reset sector - ActiveCommandParams.Sector = 1; - ActiveDrive.SectorIndex = 0; - } - else - { - ActiveDrive.SectorIndex++; - } - - UnSetBit(SR0_IC1, ref Status0); - UnSetBit(SR0_IC0, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - ActivePhase = Phase.Execution; - } - - if (ActivePhase == Phase.Execution) - { - ExecLength = buffPos; - ExecCounter = buffPos; - - DriveLight = true; - } - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - - var index = ExecLength - ExecCounter; - - LastSectorDataReadByte = ExecBuffer[index]; - - OverrunCounter--; - ExecCounter--; - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Read ID - /// COMMAND: 1 parameter byte - /// EXECUTION: The first correct ID information on the cylinder is stored in the data register - /// RESULT: 7 result bytes - /// - private void UPD_ReadID() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - DriveLight = true; - - // all parameter bytes received - ClearResultBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // set unit select - //SetUnitSelect(ActiveDrive.ID, ref Status0); - - // HD should always be 0 - UnSetBit(SR0_HD, ref Status0); - - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - // it is at this point the +3 detects whether a disk is present - // if not (and after another readid and SIS) it will eventually proceed to loading from tape - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - // setup the result buffer - ResBuffer[RS_ST0] = Status0; - for (int i = 1; i < 7; i++) - ResBuffer[i] = 0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - var track = ActiveDrive.Disk.DiskTracks.FirstOrDefault(a => a.TrackNumber == ActiveDrive.CurrentTrackID); - - if (track != null && track.NumberOfSectors > 0 && track.TrackNumber != 0xff) - { - // formatted track - - // is the index out of bounds? - if (ActiveDrive.SectorIndex >= track.NumberOfSectors) - { - // reset the index - ActiveDrive.SectorIndex = 0; - } - - if (ActiveDrive.SectorIndex == 0 && ActiveDrive.Disk.DiskTracks[ActiveDrive.CurrentTrackID].Sectors.Length > 1) - { - // looks like readid always skips the first sector on a track - ActiveDrive.SectorIndex++; - } - - // read the sector data - var data = track.Sectors[ActiveDrive.SectorIndex]; //.GetCHRN(); - ResBuffer[RS_C] = data.TrackNumber; - ResBuffer[RS_H] = data.SideNumber; - ResBuffer[RS_R] = data.SectorID; - ResBuffer[RS_N] = data.SectorSize; - - ResBuffer[RS_ST0] = Status0; - - // check for DAM & CRC - //if (data.Status2.Bit(SR2_CM)) - //SetBit(SR2_CM, ref ResBuffer[RS_ST2]); - - - // increment the current sector - ActiveDrive.SectorIndex++; - - // is the index out of bounds? - if (ActiveDrive.SectorIndex >= track.NumberOfSectors) - { - // reset the index - ActiveDrive.SectorIndex = 0; - } - } - else - { - // unformatted track? - CommitResultCHRN(); - - SetBit(SR0_IC0, ref Status0); - ResBuffer[RS_ST0] = Status0; - ResBuffer[RS_ST1] = 0x01; - } - - ActivePhase = Phase.Result; - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Write Data - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data transfer between FDC and FDD - /// RESULT: 7 result bytes - /// - private void UPD_WriteData() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - setup for execution phase - - // clear exec buffer and status registers - ClearExecBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // temp sector index - byte secIdx = ActiveCommandParams.Sector; - - // hack for when another drive (non-existent) is being called - if (ActiveDrive.ID != 0) - DiskDriveIndex = 0; - - // do we have a valid disk inserted? - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - // check write protect tab - if (ActiveDrive.FLAG_WRITEPROTECT) - { - SetBit(SR0_IC0, ref Status0); - SetBit(SR1_NW, ref Status1); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - else - { - - // calculate the number of bytes to write - int byteCounter = 0; - byte startSecID = ActiveCommandParams.Sector; - byte endSecID = ActiveCommandParams.EOT; - bool lastSec = false; - - // get the first sector - var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; - //int secIndex = 0; - for (int s = 0; s < track.Sectors.Length; s++) - { - if (track.Sectors[s].SectorID == endSecID) - lastSec = true; - - for (int i = 0; i < 0x80 << ActiveCommandParams.SectorSize; i++) - { - byteCounter++; - - if (i == (0x80 << ActiveCommandParams.SectorSize) - 1 && lastSec) - { - break; - } - } - - if (lastSec) - break; - } - - ExecCounter = byteCounter; - ExecLength = byteCounter; - ActivePhase = Phase.Execution; - DriveLight = true; - break; - } - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - - var index = ExecLength - ExecCounter; - - ExecBuffer[index] = LastSectorDataWriteByte; - - OverrunCounter--; - ExecCounter--; - - if (ExecCounter <= 0) - { - int cnt = 0; - - // all data received - byte startSecID = ActiveCommandParams.Sector; - byte endSecID = ActiveCommandParams.EOT; - bool lastSec = false; - var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; - //int secIndex = 0; - - for (int s = 0; s < track.Sectors.Length; s++) - { - if (cnt == ExecLength) - break; - - ActiveCommandParams.Sector = track.Sectors[s].SectorID; - - if (track.Sectors[s].SectorID == endSecID) - lastSec = true; - - int size = 0x80 << track.Sectors[s].SectorSize; - - for (int d = 0; d < size; d++) - { - track.Sectors[s].SectorData[d] = ExecBuffer[cnt++]; - } - - if (lastSec) - break; - } - - SetBit(SR0_IC0, ref Status0); - SetBit(SR1_EN, ref Status1); - - CommitResultCHRN(); - CommitResultStatus(); - } - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Write ID (format write) - /// COMMAND: 5 parameter bytes - /// EXECUTION: Entire track is formatted - /// RESULT: 7 result bytes - /// - private void UPD_WriteID() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - setup for execution phase - DriveLight = true; - - // clear exec buffer and status registers - ClearExecBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // temp sector index - byte secIdx = ActiveCommandParams.Sector; - - // hack for when another drive (non-existent) is being called - if (ActiveDrive.ID != 0) - DiskDriveIndex = 0; - - // do we have a valid disk inserted? - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - // check write protect tab - if (ActiveDrive.FLAG_WRITEPROTECT) - { - SetBit(SR0_IC0, ref Status0); - SetBit(SR1_NW, ref Status1); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - else - { - // not implemented yet - SetBit(SR0_IC0, ref Status0); - SetBit(SR1_NW, ref Status1); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Write Deleted Data - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data transfer between FDC and FDD - /// RESULT: 7 result bytes - /// - private void UPD_WriteDeletedData() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - setup for execution phase - - // clear exec buffer and status registers - ClearExecBuffer(); - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - // temp sector index - byte secIdx = ActiveCommandParams.Sector; - - // hack for when another drive (non-existent) is being called - if (ActiveDrive.ID != 0) - DiskDriveIndex = 0; - - // do we have a valid disk inserted? - if (!ActiveDrive.FLAG_READY) - { - // no disk, no tracks or motor is not on - SetBit(SR0_IC0, ref Status0); - SetBit(SR0_NR, ref Status0); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - - // check write protect tab - if (ActiveDrive.FLAG_WRITEPROTECT) - { - SetBit(SR0_IC0, ref Status0); - SetBit(SR1_NW, ref Status1); - - CommitResultCHRN(); - CommitResultStatus(); - //ResBuffer[RS_ST0] = Status0; - - // move to result phase - ActivePhase = Phase.Result; - break; - } - else - { - - // calculate the number of bytes to write - int byteCounter = 0; - byte startSecID = ActiveCommandParams.Sector; - byte endSecID = ActiveCommandParams.EOT; - bool lastSec = false; - - // get the first sector - var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; - //int secIndex = 0; - for (int s = 0; s < track.Sectors.Length; s++) - { - if (track.Sectors[s].SectorID == endSecID) - lastSec = true; - - for (int i = 0; i < 0x80 << ActiveCommandParams.SectorSize; i++) - { - byteCounter++; - - if (i == (0x80 << ActiveCommandParams.SectorSize) - 1 && lastSec) - { - break; - } - } - - if (lastSec) - break; - } - - ExecCounter = byteCounter; - ExecLength = byteCounter; - ActivePhase = Phase.Execution; - DriveLight = true; - break; - } - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - - var index = ExecLength - ExecCounter; - - ExecBuffer[index] = LastSectorDataWriteByte; - - OverrunCounter--; - ExecCounter--; - - if (ExecCounter <= 0) - { - int cnt = 0; - - // all data received - byte startSecID = ActiveCommandParams.Sector; - byte endSecID = ActiveCommandParams.EOT; - bool lastSec = false; - var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; - //int secIndex = 0; - - for (int s = 0; s < track.Sectors.Length; s++) - { - if (cnt == ExecLength) - break; - - ActiveCommandParams.Sector = track.Sectors[s].SectorID; - - if (track.Sectors[s].SectorID == endSecID) - lastSec = true; - - int size = 0x80 << track.Sectors[s].SectorSize; - - for (int d = 0; d < size; d++) - { - track.Sectors[s].SectorData[d] = ExecBuffer[cnt++]; - } - - if (lastSec) - break; - } - - SetBit(SR0_IC0, ref Status0); - SetBit(SR1_EN, ref Status1); - - CommitResultCHRN(); - CommitResultStatus(); - } - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Scan Equal - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data compared between the FDD and FDC - /// RESULT: 7 result bytes - /// - private void UPD_ScanEqual() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Scan Low or Equal - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data compared between the FDD and FDC - /// RESULT: 7 result bytes - /// - private void UPD_ScanLowOrEqual() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Scan High or Equal - /// COMMAND: 8 parameter bytes - /// EXECUTION: Data compared between the FDD and FDC - /// RESULT: 7 result bytes - /// - private void UPD_ScanHighOrEqual() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Specify - /// COMMAND: 2 parameter bytes - /// EXECUTION: NO execution phase - /// RESULT: NO result phase - /// - /// Looks like specify command returns status 0x80 throughout its lifecycle - /// so CB is NOT set - /// - private void UPD_Specify() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - byte currByte = CommBuffer[CommCounter]; - BitArray bi = new BitArray(new byte[] { currByte }); - - switch (CommCounter) - { - // SRT & HUT - case 0: - SRT = 16 - (currByte >> 4) & 0x0f; - HUT = (currByte & 0x0f) << 4; - if (HUT == 0) - { - HUT = 255; - } - break; - // HLT & ND - case 1: - if (bi[0]) - ND = true; - else - ND = false; - - HLT = currByte & 0xfe; - if (HLT == 0) - { - HLT = 255; - } - break; - } - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - ActivePhase = Phase.Idle; - } - - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Seek - /// COMMAND: 2 parameter bytes - /// EXECUTION: Head is positioned over proper cylinder on disk - /// RESULT: NO result phase - /// - private void UPD_Seek() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - byte currByte = CommBuffer[CommCounter]; - switch (CommCounter) - { - case 0: - ParseParamByteStandard(CommCounter); - break; - case 1: - ActiveDrive.SeekingTrack = currByte; - break; - } - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - DriveLight = true; - ActivePhase = Phase.Execution; - ActiveCommand.CommandDelegate(); - } - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - // set seek flag - ActiveDrive.SeekStatus = SEEK_SEEK; - - if (ActiveDrive.CurrentTrackID == CommBuffer[CM_C]) - { - // we are already on the correct track - ActiveDrive.SectorIndex = 0; - } - else - { - // immediate seek - ActiveDrive.CurrentTrackID = CommBuffer[CM_C]; - - ActiveDrive.SectorIndex = 0; - - if (ActiveDrive.Disk.DiskTracks[ActiveDrive.CurrentTrackID].Sectors.Length > 1) - { - // always read the first sector - //ActiveDrive.SectorIndex++; - } - } - - // skip execution mode and go directly to idle - // result is determined by SIS command - ActivePhase = Phase.Idle; - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Recalibrate (seek track 0) - /// COMMAND: 1 parameter byte - /// EXECUTION: Head retracted to track 0 - /// RESULT: NO result phase - /// - private void UPD_Recalibrate() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - DriveLight = true; - ActivePhase = Phase.Execution; - ActiveCommand.CommandDelegate(); - } - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - - // immediate recalibration - ActiveDrive.TrackIndex = 0; - ActiveDrive.SectorIndex = 0; - - // recalibrate appears to always skip the first sector - //if (ActiveDrive.Disk.DiskTracks[ActiveDrive.TrackIndex].Sectors.Length > 1) - //ActiveDrive.SectorIndex++; - - // set seek flag - ActiveDrive.SeekStatus = SEEK_RECALIBRATE; - - // skip execution mode and go directly to idle - // result is determined by SIS command - ActivePhase = Phase.Idle; - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Sense Interrupt Status - /// COMMAND: NO parameter bytes - /// EXECUTION: NO execution phase - /// RESULT: 2 result bytes - /// - private void UPD_SenseInterruptStatus() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - // SIS should return 2 bytes if sucessfully sensed an interrupt - // 1 byte otherwise - - // it seems like the +3 ROM makes 3 SIS calls for each seek/recalibrate call for some reason - // possibly one for each drive??? - // 1 - the interrupt is acknowleged with ST0 = 32 and track number - // 2 - second sis returns 1 ST0 byte with 192 - // 3 - third SIS call returns standard 1 byte 0x80 (unknown cmd or SIS with no interrupt occured) - // for now I will assume that the first call is aimed at DriveA, the second at DriveB (which we are NOT implementing) - - // check active drive first - if (ActiveDrive.SeekStatus is SEEK_RECALIBRATE or SEEK_SEEK) - { - // interrupt has been raised for this drive - // acknowledge - ActiveDrive.SeekStatus = SEEK_IDLE;// SEEK_INTACKNOWLEDGED; - - // result length 2 - ResLength = 2; - - // first byte ST0 0x20 - Status0 = 0x20; - ResBuffer[0] = Status0; - // second byte is the current track id - ResBuffer[1] = ActiveDrive.CurrentTrackID; - } - /* - else if (ActiveDrive.SeekStatus == SEEK_INTACKNOWLEDGED) - { - // DriveA interrupt has already been acknowledged - ActiveDrive.SeekStatus = SEEK_IDLE; - - ResLength = 1; - Status0 = 192; - ResBuffer[0] = Status0; - } - */ - else if (ActiveDrive.SeekStatus == SEEK_IDLE) - { - // SIS with no interrupt - ResLength = 1; - Status0 = 0x80; - ResBuffer[0] = Status0; - } - - ActivePhase = Phase.Result; - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Sense Drive Status - /// COMMAND: 1 parameter byte - /// EXECUTION: NO execution phase - /// RESULT: 1 result byte - /// - /// The ZX spectrum appears to only specify drive 1 as the parameter byte, NOT drive 0 - /// After the final param byte is received main status changes to 0xd0 - /// Data register (ST3) result is 0x51 if drive/disk not available - /// 0x71 if disk is present in 2nd drive - /// - private void UPD_SenseDriveStatus() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - // store the parameter in the command buffer - CommBuffer[CommCounter] = LastByteReceived; - - // process parameter byte - ParseParamByteStandard(CommCounter); - - // increment command parameter counter - CommCounter++; - - // was that the last parameter byte? - if (CommCounter == ActiveCommand.ParameterByteCount) - { - // all parameter bytes received - ActivePhase = Phase.Execution; - UPD_SenseDriveStatus(); - } - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - // one ST3 byte required - - // set US - Status3 = (byte)ActiveDrive.ID; - - if (Status3 != 0) - { - // we only support 1 drive - SetBit(SR3_FT, ref Status3); - } - else - { - // HD - only one side - UnSetBit(SR3_HD, ref Status3); - - // write protect - if (ActiveDrive.FLAG_WRITEPROTECT) - SetBit(SR3_WP, ref Status3); - - // track 0 - if (ActiveDrive.FLAG_TRACK0) - SetBit(SR3_T0, ref Status3); - - // rdy - if (ActiveDrive.Disk != null) - SetBit(SR3_RY, ref Status3); - } - - ResBuffer[0] = Status3; - ActivePhase = Phase.Result; - - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - break; - } - } - - /// - /// Version - /// COMMAND: NO parameter bytes - /// EXECUTION: NO execution phase - /// RESULT: 1 result byte - /// - private void UPD_Version() - { - switch (ActivePhase) - { - case Phase.Idle: - case Phase.Command: - case Phase.Execution: - case Phase.Result: - UPD_Invalid(); - break; - } - } - - /// - /// Invalid - /// COMMAND: NO parameter bytes - /// EXECUTION: NO execution phase - /// RESULT: 1 result byte - /// - private void UPD_Invalid() - { - switch (ActivePhase) - { - //---------------------------------------- - // FDC is waiting for a command byte - //---------------------------------------- - case Phase.Idle: - break; - - //---------------------------------------- - // Receiving command parameter bytes - //---------------------------------------- - case Phase.Command: - break; - - //---------------------------------------- - // FDC in execution phase reading/writing bytes - //---------------------------------------- - case Phase.Execution: - // no execution phase - ActivePhase = Phase.Result; - UPD_Invalid(); - break; - - //---------------------------------------- - // Result bytes being sent to CPU - //---------------------------------------- - case Phase.Result: - ResBuffer[0] = 0x80; - break; - } - } - - /// - /// Called when a status register read is required - /// This can be called at any time - /// The main status register appears to be queried nearly all the time - /// so needs to be kept updated. It keeps the CPU informed of the current state - /// - private byte ReadMainStatus() - { - SetBit(MSR_RQM, ref StatusMain); - - switch (ActivePhase) - { - case Phase.Idle: - UnSetBit(MSR_DIO, ref StatusMain); - UnSetBit(MSR_CB, ref StatusMain); - UnSetBit(MSR_EXM, ref StatusMain); - break; - case Phase.Command: - UnSetBit(MSR_DIO, ref StatusMain); - SetBit(MSR_CB, ref StatusMain); - UnSetBit(MSR_EXM, ref StatusMain); - break; - case Phase.Execution: - if (ActiveCommand.Direction == CommandDirection.OUT) - SetBit(MSR_DIO, ref StatusMain); - else - UnSetBit(MSR_DIO, ref StatusMain); - - SetBit(MSR_EXM, ref StatusMain); - SetBit(MSR_CB, ref StatusMain); - - // overrun detection - OverrunCounter++; - if (OverrunCounter >= 64) - { - // CPU has read the status register 64 times without reading the data register - // switch the current command into result phase - ActivePhase = Phase.Result; - - // reset the overun counter - OverrunCounter = 0; - } - - break; - case Phase.Result: - SetBit(MSR_DIO, ref StatusMain); - SetBit(MSR_CB, ref StatusMain); - UnSetBit(MSR_EXM, ref StatusMain); - break; - } - - //if (!CheckTiming()) - //{ - // UnSetBit(MSR_EXM, ref StatusMain); - //} - - return StatusMain; - } - - //private int testCount = 0; - /// - /// Handles CPU reading from the data register - /// - private byte ReadDataRegister() - { - // default return value - byte res = 0xff; - - // check RQM flag status - if (!GetBit(MSR_RQM, StatusMain)) - { - // FDC is not ready to return data - return res; - } - - // check active direction - if (!GetBit(MSR_DIO, StatusMain)) - { - // FDC is expecting to receive, not send data - return res; - } - - switch (ActivePhase) - { - case Phase.Execution: - // reset overrun counter - OverrunCounter = 0; - - // execute read - ActiveCommand.CommandDelegate(); - - res = LastSectorDataReadByte; - - if (ExecCounter <= 0) - { - // end of execution phase - ActivePhase = Phase.Result; - } - - return res; - - case Phase.Result: - - DriveLight = false; - - ActiveCommand.CommandDelegate(); - - // result byte reading - res = ResBuffer[ResCounter]; - - // increment result counter - ResCounter++; - - if (ResCounter >= ResLength) - { - ActivePhase = Phase.Idle; - } - - break; - } - - return res; - } - - /// - /// Handles CPU writing to the data register - /// - private void WriteDataRegister(byte data) - { - if (!GetBit(MSR_RQM, StatusMain) || GetBit(MSR_DIO, StatusMain)) - { - // FDC will not receive and process any bytes - return; - } - - // store the incoming byte - LastByteReceived = data; - - // process incoming bytes - switch (ActivePhase) - { - //// controller is idle awaiting the first command byte of a new instruction - case Phase.Idle: - ParseCommandByte(data); - break; - //// we are in command phase - case Phase.Command: - // attempt to process this parameter byte - //ProcessCommand(data); - ActiveCommand.CommandDelegate(); - break; - //// we are in execution phase - case Phase.Execution: - // CPU is going to be sending data bytes to the FDC to be written to disk - - // store the byte - LastSectorDataWriteByte = data; - ActiveCommand.CommandDelegate(); - - if (ExecCounter <= 0) - { - // end of execution phase - ActivePhase = Phase.Result; - } - - break; - //// result phase - case Phase.Result: - // data register will not receive bytes during result phase - break; - } - } - - /// - /// Processes the first command byte (within a command instruction) - /// Returns TRUE if successful. FALSE if otherwise - /// Called only in idle phase - /// - private bool ParseCommandByte(byte cmdByte) - { - // clear counters - CommCounter = 0; - ResCounter = 0; - - // get the first 4 bytes - byte cByte = (byte)(cmdByte & 0x0f); - - // get MT, MD and SK states - CMD_FLAG_MT = cmdByte.Bit(7); - CMD_FLAG_MF = cmdByte.Bit(6); - CMD_FLAG_SK = cmdByte.Bit(5); - - cmdByte = cByte; - - // lookup the command - var i = CommandList.FindIndex(a => a.CommandCode == cmdByte); - if (i is -1) - { - // no command found - use invalid - CMDIndex = CommandList.Count - 1; - } - else - { - // valid command found - CMDIndex = i; - - // check validity of command byte flags - // if a flag is set but not valid for this command then it is invalid - bool invalid = false; - - if (!ActiveCommand.MT) - if (CMD_FLAG_MT) - invalid = true; - if (!ActiveCommand.MF) - if (CMD_FLAG_MF) - invalid = true; - if (!ActiveCommand.SK) - if (CMD_FLAG_SK) - invalid = true; - - if (invalid) - { - // command byte included spurious bit 5,6 or 7 flags - CMDIndex = CommandList.Count - 1; - } - - /* - if ((CMD_FLAG_MF && !ActiveCommand.MF) - || (CMD_FLAG_MT && !ActiveCommand.MT) - || (CMD_FLAG_SK && !ActiveCommand.SK)) - { - // command byte included spurious bit 5,6 or 7 flags - CMDIndex = CommandList.Count - 1; - } - */ - } - - CommCounter = 0; - ResCounter = 0; - - // there will now be an active command set - // move to command phase - ActivePhase = Phase.Command; - - /* - // check for invalid SIS - if (ActiveInterrupt == InterruptState.None && CMDIndex == CC_SENSE_INTSTATUS) - { - CMDIndex = CC_INVALID; - //ActiveCommand.CommandDelegate(InstructionState.StartResult); - } - */ - - // set reslength - ResLength = ActiveCommand.ResultByteCount; - - // if there are no expected param bytes to receive - go ahead and run the command - if (ActiveCommand.ParameterByteCount == 0) - { - ActivePhase = Phase.Execution; - ActiveCommand.CommandDelegate(); - } - - return true; - } - - /// - /// Parses the first 5 command argument bytes that are of the standard format - /// - private void ParseParamByteStandard(int index) - { - byte currByte = CommBuffer[index]; - BitArray bi = new BitArray(new byte[] { currByte }); - - switch (index) - { - // HD & US - case CM_HEAD: - if (bi[2]) - ActiveCommandParams.Side = 1; - else - ActiveCommandParams.Side = 0; - - ActiveCommandParams.UnitSelect = (byte)(GetUnitSelect(currByte)); - DiskDriveIndex = ActiveCommandParams.UnitSelect; - break; - - // C - case CM_C: - ActiveCommandParams.Cylinder = currByte; - break; - - // H - case CM_H: - ActiveCommandParams.Head = currByte; - break; - - // R - case CM_R: - ActiveCommandParams.Sector = currByte; - break; - - // N - case CM_N: - ActiveCommandParams.SectorSize = currByte; - break; - - // EOT - case CM_EOT: - ActiveCommandParams.EOT = currByte; - break; - - // GPL - case CM_GPL: - ActiveCommandParams.Gap3Length = currByte; - break; - - // DTL - case CM_DTL: - ActiveCommandParams.DTL = currByte; - break; - - default: - break; - } - } - - /// - /// Clears the result buffer - /// - public void ClearResultBuffer() - { - for (int i = 0; i < ResBuffer.Length; i++) - { - ResBuffer[i] = 0; - } - } - - /// - /// Clears the result buffer - /// - public void ClearExecBuffer() - { - for (int i = 0; i < ExecBuffer.Length; i++) - { - ExecBuffer[i] = 0; - } - } - - /// - /// Populates the result status registers - /// - private void CommitResultStatus() - { - // check for read diag - if (ActiveCommand.CommandCode == 0x02) - { - // commit to result buffer - ResBuffer[RS_ST0] = Status0; - ResBuffer[RS_ST1] = Status1; - return; - } - - // check for error bits - if (GetBit(SR1_DE, Status1) - || GetBit(SR1_MA, Status1) - || GetBit(SR1_ND, Status1) - || GetBit(SR1_NW, Status1) - || GetBit(SR1_OR, Status1) - || GetBit(SR2_BC, Status2) - || GetBit(SR2_CM, Status2) - || GetBit(SR2_DD, Status2) - || GetBit(SR2_MD, Status2) - || GetBit(SR2_SN, Status2) - || GetBit(SR2_WC, Status2)) - { - // error bits set - unset end of track - UnSetBit(SR1_EN, ref Status1); - } - - // check for data errors - if (GetBit(SR1_DE, Status1) - || GetBit(SR2_DD, Status2)) - { - // unset control mark - UnSetBit(SR2_CM, ref Status2); - } - else if (GetBit(SR2_CM, Status2)) - { - // DAM found - unset IC and US0 - UnSetBit(SR0_IC0, ref Status0); - UnSetBit(SR0_US0, ref Status0); - } - - // commit to result buffer - ResBuffer[RS_ST0] = Status0; - ResBuffer[RS_ST1] = Status1; - ResBuffer[RS_ST2] = Status2; - } - - /// - /// Populates the result CHRN values - /// - private void CommitResultCHRN() - { - ResBuffer[RS_C] = ActiveCommandParams.Cylinder; - ResBuffer[RS_H] = ActiveCommandParams.Head; - ResBuffer[RS_R] = ActiveCommandParams.Sector; - ResBuffer[RS_N] = ActiveCommandParams.SectorSize; - } - - /// - /// Moves active phase into idle - /// - public void SetPhase_Idle() - { - ActivePhase = Phase.Idle; - - // active direction - UnSetBit(MSR_DIO, ref StatusMain); - // CB - UnSetBit(MSR_CB, ref StatusMain); - // RQM - SetBit(MSR_RQM, ref StatusMain); - - CommCounter = 0; - ResCounter = 0; - } - - /// - /// Moves to result phase - /// - public void SetPhase_Result() - { - ActivePhase = Phase.Result; - - // active direction - SetBit(MSR_DIO, ref StatusMain); - // CB - SetBit(MSR_CB, ref StatusMain); - // RQM - SetBit(MSR_RQM, ref StatusMain); - // EXM - UnSetBit(MSR_EXM, ref StatusMain); - - CommCounter = 0; - ResCounter = 0; - } - - /// - /// Moves to command phase - /// - public void SetPhase_Command() - { - ActivePhase = Phase.Command; - - // default 0x80 - just RQM - SetBit(MSR_RQM, ref StatusMain); - UnSetBit(MSR_DIO, ref StatusMain); - UnSetBit(MSR_CB, ref StatusMain); - UnSetBit(MSR_EXM, ref StatusMain); - CommCounter = 0; - ResCounter = 0; - } - - /// - /// Moves to execution phase - /// - public void SetPhase_Execution() - { - ActivePhase = Phase.Execution; - - // EXM - SetBit(MSR_EXM, ref StatusMain); - // CB - SetBit(MSR_CB, ref StatusMain); - // RQM - UnSetBit(MSR_RQM, ref StatusMain); - - CommCounter = 0; - ResCounter = 0; - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.FDD.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.FDD.cs deleted file mode 100644 index 3768a0694a1..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.FDD.cs +++ /dev/null @@ -1,830 +0,0 @@ -using BizHawk.Common; -using BizHawk.Common.NumberExtensions; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Floppy drive related stuff - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 : IFDDHost - { - /// - /// FDD Flag - motor on/off - /// - public bool FDD_FLAG_MOTOR; - - /// - /// The index of the currently active disk drive - /// - public int DiskDriveIndex - { - get => _diskDriveIndex; - set - { - // when index is changed update the ActiveDrive - _diskDriveIndex = value; - ActiveDrive = DriveStates[_diskDriveIndex]; - } - } - private int _diskDriveIndex = 0; - - /// - /// The currently active drive - /// - private DriveState ActiveDrive; - - /// - /// Array that holds state information for each possible drive - /// - private readonly DriveState[] DriveStates = new DriveState[4]; - - /// - /// Initialization / reset of the floppy drive subsystem - /// - private void FDD_Init() - { - for (int i = 0; i < 4; i++) - { - DriveState ds = new DriveState(i, this); - DriveStates[i] = ds; - } - } - - /// - /// Searches for the requested sector - /// - private FloppyDisk.Sector GetSector() - { - FloppyDisk.Sector sector = null; - - // get the current track - var trk = ActiveDrive.Disk.DiskTracks[ActiveDrive.TrackIndex]; - - // get the current sector index - int index = ActiveDrive.SectorIndex; - - // make sure this index exists - if (index > trk.Sectors.Length) - { - index = 0; - } - - // index hole count - int iHole = 0; - - // loop through the sectors in a track - // the loop ends with either the sector being found - // or the index hole being passed twice - while (iHole <= 2) - { - var next = trk.Sectors[index]; - // does the requested sector match the current sector - if (next.SectorIDInfo.C == ActiveCommandParams.Cylinder - && next.SectorIDInfo.H == ActiveCommandParams.Head - && next.SectorIDInfo.R == ActiveCommandParams.Sector - && next.SectorIDInfo.N == ActiveCommandParams.SectorSize) - { - // sector has been found - sector = next; - - UnSetBit(SR2_BC, ref Status2); - UnSetBit(SR2_WC, ref Status2); - break; - } - - // check for bad cylinder - if (next.SectorIDInfo.C is 255) - { - SetBit(SR2_BC, ref Status2); - } - // check for no cylinder - else if (next.SectorIDInfo.C != ActiveCommandParams.Cylinder) - { - SetBit(SR2_WC, ref Status2); - } - - // incrememnt sector index - index++; - - // have we reached the index hole? - if (trk.Sectors.Length <= index) - { - // wrap around - index = 0; - iHole++; - } - } - - // search loop has completed and the sector may or may not have been found - - // bad cylinder detected? - if (Status2.Bit(SR2_BC)) - { - // remove WC - UnSetBit(SR2_WC, ref Status2); - } - - // update sectorindex on drive - ActiveDrive.SectorIndex = index; - - return sector; - } - - // IFDDHost methods that fall through to the currently active drive - - /// - /// Parses a new disk image and loads it into this floppy drive - /// - public void FDD_LoadDisk(byte[] diskData) - { - // we are only going to load into the first drive - DriveStates[0].FDD_LoadDisk(diskData); - } - - /// - /// Ejects the current disk - /// - public void FDD_EjectDisk() - { - DriveStates[0].FDD_EjectDisk(); - } - - /// - /// Signs whether the current active drive has a disk inserted - /// - public bool FDD_IsDiskLoaded => DriveStates[DiskDriveIndex].FDD_IsDiskLoaded; - - /// - /// Returns the disk object from drive 0 - /// - public FloppyDisk DiskPointer => DriveStates[0].Disk; - - public FloppyDisk Disk { get; set; } - - /// - /// Holds specfic state information about a drive - /// - private sealed class DriveState : IFDDHost - { - /// - /// The drive ID from an FDC perspective - /// - public int ID; - - /// - /// Signs whether this drive ready - /// TRUE if both drive exists and has a disk inserted - /// - public bool FLAG_READY - { - get - { - if (!FDD_IsDiskLoaded || Disk.GetTrackCount() == 0 || !FDC.FDD_FLAG_MOTOR) - return false; - else - return true; - } - } - - /// - /// Disk is write protected (TRUE BY DEFAULT) - /// - public bool FLAG_WRITEPROTECT = false; - - /// - /// Storage for seek steps - /// One step for each indexpulse (track index) until seeked track - /// - public int SeekCounter; - - /// - /// Seek status - /// - public int SeekStatus; - - /// - /// Age counter - /// - public int SeekAge; - - /// - /// The current side - /// - public byte CurrentSide; - - /// - /// The current track index in the DiskTracks array - /// - public byte TrackIndex; - - /// - /// The track ID of the current cylinder - /// - public byte CurrentTrackID - { - get - { - // default invalid track - int id = 0xff; - - if (Disk == null) - return (byte)id; - - if (Disk.DiskTracks.Length == 0) - return (byte)id; - - if (TrackIndex >= Disk.GetTrackCount()) - TrackIndex = 0; - else if (TrackIndex < 0) - TrackIndex = 0; - - var track = Disk.DiskTracks[TrackIndex]; - - id = track.TrackNumber; - - return (byte)id; - } - set - { - for (int i = 0; i < Disk.GetTrackCount(); i++) - { - if (Disk.DiskTracks[i].TrackNumber == value) - { - TrackIndex = (byte)i; - break; - } - } - } - } - - - /// - /// The new track that the drive is seeking to - /// (used in seek operations) - /// - public int SeekingTrack; - - /// - /// The current sector index in the Sectors array - /// - public int SectorIndex; - - /// - /// The currently loaded floppy disk - /// - public FloppyDisk Disk { get; set; } - - /// - /// The parent controller - /// - private readonly NECUPD765 FDC; - - /// - /// TRUE if we are on track 0 - /// - public bool FLAG_TRACK0 => TrackIndex == 0; - - /* - /// - /// Moves the head across the disk cylinders - /// - public void MoveHead(SkipDirection direction, int cylinderCount) - { - // get total tracks - int trackCount = Disk.DiskTracks.Length; - - int trk = 0; - - switch (direction) - { - case SkipDirection.Increment: - trk = (int)CurrentTrack + cylinderCount; - if (trk >= trackCount) - { - // past the last track - trk = trackCount - 1; - } - else if (trk < 0) - trk = 0; - break; - case SkipDirection.Decrement: - trk = (int)CurrentTrack - cylinderCount; - if (trk < 0) - { - // before the first track - trk = 0; - } - else if (trk >= trackCount) - trk = trackCount - 1; - break; - } - - // move the head - CurrentTrack = (byte)trk; - } - */ - - /* - - /// - /// Finds a supplied sector - /// - public FloppyDisk.Sector FindSector(ref byte[] resBuffer, CommandParameters prms) - { - int index =CurrentSector; - int lc = 0; - FloppyDisk.Sector sector = null; - - bool found = false; - - do - { - sector = Disk.DiskTracks[CurrentTrack].Sectors[index]; - if (sector != null && sector.SectorID == prms.Sector) - { - // sector found - // check for data errors - if ((sector.Status1 & 0x20) != 0 || (sector.Status2 & 0x20) != 0) - { - // data errors found - } - found = true; - break; - } - - // sector doesnt match - var c = Disk.DiskTracks[CurrentTrack].Sectors[index].TrackNumber; - if (c == 255) - { - // bad cylinder - resBuffer[RS_ST2] |= 0x02; - } - else if (prms.Cylinder != c) - { - // cylinder mismatch - resBuffer[RS_ST2] |= 0x10; - } - - // increment index - index++; - - if (index >= Disk.DiskTracks[CurrentTrack].NumberOfSectors) - { - // out of bounds - index = 0; - lc++; - } - - } while (lc < 2); - - if ((resBuffer[RS_ST2] & 0x02) != 0) - { - // bad cylinder set - remove no cylinder - UnSetBit(SR2_WC, ref resBuffer[RS_ST2]); - } - - // update current sector - CurrentSector = index; - - if (found) - return sector; - else - return null; - } - - - /// - /// Populates a result buffer - /// - public void FillResult(ref byte[] resBuffer, CHRN chrn) - { - // clear results - resBuffer[RS_ST0] = 0; - resBuffer[RS_ST1] = 0; - resBuffer[RS_ST2] = 0; - resBuffer[RS_C] = 0; - resBuffer[RS_H] = 0; - resBuffer[RS_R] = 0; - resBuffer[RS_N] = 0; - - if (chrn == null) - { - // no chrn supplied - resBuffer[RS_ST0] = ST0; - resBuffer[RS_ST1] = 0; - resBuffer[RS_ST2] = 0; - resBuffer[RS_C] = 0; - resBuffer[RS_H] = 0; - resBuffer[RS_R] = 0; - resBuffer[RS_N] = 0; - } - } - - - - /// - /// Populates the result buffer with ReadID data - /// - public void ReadID(ref byte[] resBuffer) - { - if (CheckDriveStatus() == false) - { - // drive not ready - resBuffer[RS_ST0] = ST0; - return; - } - - var track = Disk.DiskTracks.Where(a => a.TrackNumber == CurrentTrack).FirstOrDefault(); - - if (track != null && track.NumberOfSectors > 0) - { - // formatted track - - // get the current sector - int index = CurrentSector; - - // is the index out of bounds? - if (index >= track.NumberOfSectors) - { - // reset the index - index = 0; - } - - // read the sector data - var data = track.Sectors[index]; - resBuffer[RS_C] = data.TrackNumber; - resBuffer[RS_H] = data.SideNumber; - resBuffer[RS_R] = data.SectorID; - resBuffer[RS_N] = data.SectorSize; - - resBuffer[RS_ST0] = ST0; - - // increment the current sector - CurrentSector = index + 1; - return; - } - else - { - // unformatted track? - resBuffer[RS_C] = FDC.CommBuffer[CM_C]; - resBuffer[RS_H] = FDC.CommBuffer[CM_H]; - resBuffer[RS_R] = FDC.CommBuffer[CM_R]; - resBuffer[RS_N] = FDC.CommBuffer[CM_N]; - - SetBit(SR0_IC0, ref ST0); - resBuffer[RS_ST0] = ST0; - resBuffer[RS_ST1] = 0x01; - return; - } - } - */ - - /* - - /// - /// The drive performs a seek operation if necessary - /// Return value TRUE indicates seek complete - /// - public void DoSeek() - { - if (CurrentState is not (DriveMainState.Recalibrate or DriveMainState.Seek)) - { - // no seek/recalibrate has been asked for - return; - } - - if (GetBit(ID, FDC.StatusMain)) - { - // drive is already seeking - return; - } - - RunSeekCycle(); - } - - /// - /// Runs a seek cycle - /// - public void RunSeekCycle() - { - for (;;) - { - switch (SeekState) - { - // seek or recalibrate has been requested - case SeekSubState.Idle: - - if (CurrentState == DriveMainState.Recalibrate) - { - // recalibrate always seeks to track 0 - SeekingTrack = 0; - } - SeekState = SeekSubState.MoveInit; - - // mark drive as busy - // this should be cleared by SIS command - SetBit(ID, ref FDC.StatusMain); - - break; - - // setup for the head move - case SeekSubState.MoveInit: - - if (CurrentTrack == SeekingTrack) - { - // we are already at the required track - if (CurrentState is DriveMainState.Recalibrate && !FLAG_TRACK0) - { - // recalibration fail - SeekIntState = SeekIntStatus.Abnormal; - - // raise seek interrupt - FDC.ActiveInterrupt = InterruptState.Seek; - - // unset DB bit - UnSetBit(ID, ref FDC.StatusMain); - - // equipment check - SetBit(SR0_EC, ref FDC.Status0); - - SeekState = SeekSubState.PerformCompletion; - break; - } - - if (CurrentState is DriveMainState.Recalibrate && FLAG_TRACK0) - { - // recalibration success - SeekIntState = SeekIntStatus.Normal; - - // raise seek interrupt - FDC.ActiveInterrupt = InterruptState.Seek; - - // unset DB bit - UnSetBit(ID, ref FDC.StatusMain); - - SeekState = SeekSubState.PerformCompletion; - break; - } - } - - // check for error - if (IntStatus >= IC_ABORTED_DISCREMOVED || Disk == null) - { - // drive not ready - FLAG_READY = false; - - // drive not ready - SeekIntState = SeekIntStatus.DriveNotReady; - - // cancel any interrupt - FDC.ActiveInterrupt = InterruptState.None; - - // unset DB bit - UnSetBit(ID, ref FDC.StatusMain); - - SeekState = SeekSubState.PerformCompletion; - break; - } - - if (SeekCounter > 1) - { - // not ready to seek yet - SeekCounter--; - return; - } - - if (FDC.SRT < 1 && CurrentTrack != SeekingTrack) - { - SeekState = SeekSubState.MoveImmediate; - break; - } - - // head move - SeekState = SeekSubState.HeadMove; - - break; - - case SeekSubState.HeadMove: - - // do the seek - SeekCounter = FDC.SRT; - - if (CurrentTrack < SeekingTrack) - { - // we are seeking forward - var delta = SeekingTrack - CurrentTrack; - MoveHead(SkipDirection.Increment, 1); - } - else if (CurrentTrack > SeekingTrack) - { - // we are seeking backward - var delta = CurrentTrack - SeekingTrack; - MoveHead(SkipDirection.Decrement, 1); - } - - // should the seek be completed now? - if (CurrentTrack == SeekingTrack) - { - SeekState = SeekSubState.PerformCompletion; - break; - } - - // seek not finished yet - return; - - // seek emulation processed immediately - case SeekSubState.MoveImmediate: - - if (CurrentTrack < SeekingTrack) - { - // we are seeking forward - var delta = SeekingTrack - CurrentTrack; - MoveHead(SkipDirection.Increment, delta); - - } - else if (CurrentTrack > SeekingTrack) - { - // we are seeking backward - var delta = CurrentTrack - SeekingTrack; - MoveHead(SkipDirection.Decrement, delta); - } - - SeekState = SeekSubState.PerformCompletion; - break; - - case SeekSubState.PerformCompletion: - SeekDone(); - SeekState = SeekSubState.SeekCompleted; - break; - - case SeekSubState.SeekCompleted: - // seek has already completed - return; - } - } - } - - /// - /// Called when a seek operation has completed - /// - public void SeekDone() - { - SeekCounter = 0; - SeekingTrack = CurrentTrack; - - // generate ST0 register data - - // get only the IC bits - IntStatus &= IC_ABORTED_DISCREMOVED; - - // drive ready? - if (!FLAG_READY) - { - SetBit(SR0_NR, ref IntStatus); - SetBit(SR0_EC, ref IntStatus); - - // are we recalibrating? - if (CurrentState == DriveMainState.Recalibrate) - { - SetBit(SR0_EC, ref IntStatus); - } - } - - // set seek end - SetBit(SR0_SE, ref IntStatus); - /* - // head address - if (CurrentSide > 0) - { - SetBit(SR0_HD, ref IntStatus); - - // drive only supports 1 head - // set the EC bit - SetBit(SR0_EC, ref IntStatus); - } - */ - /* - // UnitSelect - SetUnitSelect(ID, ref IntStatus); - - // move to none state - //CurrentState = DriveMainState.None; - - //SeekState = SeekSubState.SeekCompleted; - - // set the seek interrupt flag for this drive - // this will be cleared at the next successful senseint - FLAG_SEEK_INTERRUPT = true; - - //CurrentState = DriveMainState.None; - - } - */ - - public DriveState(int driveID, NECUPD765 fdc) - { - ID = driveID; - FDC = fdc; - } - - /// - /// Parses a new disk image and loads it into this floppy drive - /// - public void FDD_LoadDisk(byte[] diskData) - { - // try dsk first - FloppyDisk fdd = null; - bool found = false; - - foreach (DiskType type in Enum.GetValues(typeof(DiskType))) - { - switch (type) - { - case DiskType.CPCExtended: - fdd = new CPCExtendedFloppyDisk(); - found = fdd.ParseDisk(diskData); - break; - case DiskType.CPC: - fdd = new CPCFloppyDisk(); - found = fdd.ParseDisk(diskData); - break; - case DiskType.IPF: - fdd = new IPFFloppyDisk(); - found = fdd.ParseDisk(diskData); - break; - case DiskType.UDI: - fdd = new UDI1_0FloppyDisk(); - found = fdd.ParseDisk(diskData); - break; - } - - if (found) - { - Disk = fdd; - break; - } - } - - if (!found) - { - throw new Exception($"{nameof(DriveState)}\n\nDisk image file could not be parsed. Potentially an unknown format."); - } - } - - /// - /// Ejects the current disk - /// - public void FDD_EjectDisk() - { - Disk = null; - //FLAG_READY = false; - } - - /// - /// Signs whether the current active drive has a disk inserted - /// - public bool FDD_IsDiskLoaded - { - get - { - if (Disk != null) - return true; - else - return false; - } - } - - public void SyncState(Serializer ser) - { - ser.Sync(nameof(ID), ref ID); - ser.Sync(nameof(FLAG_WRITEPROTECT), ref FLAG_WRITEPROTECT); - //ser.Sync(nameof(FLAG_DISKCHANGED), ref FLAG_DISKCHANGED); - //ser.Sync(nameof(FLAG_RECALIBRATING), ref FLAG_RECALIBRATING); - //ser.Sync(nameof(FLAG_SEEK_INTERRUPT), ref FLAG_SEEK_INTERRUPT); - //ser.Sync(nameof(IntStatus), ref IntStatus); - //ser.Sync(nameof(ST0), ref ST0); - //ser.Sync(nameof(RecalibrationCounter), ref RecalibrationCounter); - ser.Sync(nameof(SeekCounter), ref SeekCounter); - ser.Sync(nameof(SeekStatus), ref SeekStatus); - ser.Sync(nameof(SeekAge), ref SeekAge); - ser.Sync(nameof(CurrentSide), ref CurrentSide); - //ser.Sync(nameof(CurrentTrack), ref CurrentTrack); - ser.Sync(nameof(TrackIndex), ref TrackIndex); - ser.Sync(nameof(SeekingTrack), ref SeekingTrack); - //ser.Sync(nameof(CurrentSector), ref CurrentSector); - ser.Sync(nameof(SectorIndex), ref SectorIndex); - //ser.Sync(nameof(RAngles), ref RAngles); - //ser.Sync(nameof(DataPointer), ref DataPointer); - //ser.SyncEnum(nameof(CurrentState), ref CurrentState); - //ser.SyncEnum(nameof(SeekState), ref SeekState); - //ser.SyncEnum(nameof(SeekIntState), ref SeekIntState); - } - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.IPortIODevice.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.IPortIODevice.cs deleted file mode 100644 index 2cca36eb434..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.IPortIODevice.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// IPortIODevice - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 : IPortIODevice - { - public string outputfile = @"D:\Dropbox\Dropbox\_Programming\TASVideos\BizHawk\output\zxhawkio-" + DateTime.Now.ToString("yyyyMMdd_HHmmss", DateTimeFormatInfo.InvariantInfo) + ".csv"; - public string outputString = "STATUS,WRITE,READ,CODE,MT,MF,SK,CMDCNT,RESCNT,EXECCNT,EXECLEN\r\n"; - public bool writeDebug = false; - - public List dLog = new List - { - "STATUS,WRITE,READ,CODE,MT,MF,SK,CMDCNT,RESCNT,EXECCNT,EXECLEN" - }; - - - /* - * Status read - * Data write - * Data read - * CMD code - * CMD string - * MT flag - * MK flag - * SK flag - * */ - private readonly string[] workingArr = new string[3]; - - private void BuildCSVLine() - { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 3; i++) - { - sb.Append(workingArr[i]); - sb.Append(','); - workingArr[i] = ""; - } - - sb.Append(ActiveCommand.CommandCode).Append(','); - - sb.Append(CMD_FLAG_MT).Append(','); - sb.Append(CMD_FLAG_MF).Append(','); - sb.Append(CMD_FLAG_SK).Append(','); - - sb.Append(CommCounter).Append(','); - sb.Append(ResCounter).Append(','); - sb.Append(ExecCounter).Append(','); - sb.Append(ExecLength); - - //sb.Append("\r\n"); - - //outputString += sb.ToString(); - dLog.Add(sb.ToString()); - } - - /// - /// Device responds to an IN instruction - /// - public bool ReadPort(ushort port, ref int data) - { - BitArray bits = new BitArray(new byte[] { (byte)data }); - - if (port == 0x3ffd) - { - // Z80 is trying to read from the data register - data = ReadDataRegister(); - if (writeDebug) - { - workingArr[2] = data.ToString(); - //outputString += ",," + data + "," + ActiveCommand.CommandCode + "\r\n"; - BuildCSVLine(); - } - - return true; - } - - if (port == 0x2ffd) - { - // read main status register - // this can happen at any time - data = ReadMainStatus(); - if (writeDebug) - { - //outputString += data + ",,," + ActiveCommand.CommandCode + "\r\n"; - workingArr[0] = data.ToString(); - BuildCSVLine(); - //System.IO.File.WriteAllText(outputfile, outputString); - } - - return true; - } - - return false; - } - - /// - /// Device responds to an OUT instruction - /// - public bool WritePort(ushort port, int data) - { - BitArray bits = new BitArray(new byte[] { (byte)data }); - - if (port == 0x3ffd) - { - // Z80 is attempting to write to the data register - WriteDataRegister((byte)data); - if (writeDebug) - { - //outputString += "," + data + ",," + ActiveCommand.CommandCode + "\r\n"; - workingArr[1] = data.ToString(); - BuildCSVLine(); - //System.IO.File.WriteAllText(outputfile, outputString); - } - - return true; - } - - if (port == 0x1ffd) - { - // set disk motor on/off - FDD_FLAG_MOTOR = bits[3]; - return true; - } - return false; - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.Timing.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.Timing.cs deleted file mode 100644 index dd09a675b82..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.Timing.cs +++ /dev/null @@ -1,117 +0,0 @@ - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Timimng - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 - { - /// - /// The current Z80 cycle - /// - private long CurrentCPUCycle - { - get - { - if (_machine == null) - return 0; - else - return _machine.CPU.TotalExecutedCycles; - } - } - - /// - /// The last CPU cycle when the FDC accepted an IO read/write - /// - private long LastCPUCycle; - - /// - /// The current delay figure (in Z80 t-states) - /// This implementation only introduces delay upon main status register reads - /// All timing calculations should be done during the other read/write operations - /// - private long StatusDelay; - - /// - /// Defines the numbers of Z80 cycles per MS - /// - private long CPUCyclesPerMs; - - /// - /// The floppy drive emulated clock speed - /// - public const double DriveClock = 31250; - - /// - /// The number of floppy drive cycles per MS - /// - public long DriveCyclesPerMs; - - /// - /// The number of T-States in one floppy drive clock tick - /// - public long StatesPerDriveTick; - - /// - /// Responsible for measuring when the floppy drive is ready to run a cycle - /// - private long TickCounter; - - /// - /// Internal drive cycle counter - /// - private int DriveCycleCounter = 1; - - /// - /// Initializes the timing routines - /// - private void TimingInit() - { - // z80 timing - double frameSize = _machine.ULADevice.FrameLength; - double rRate = _machine.ULADevice.ClockSpeed / frameSize; - long tPerSecond = (long)(frameSize * rRate); - CPUCyclesPerMs = tPerSecond / 1000; - - // drive timing - double dRate = DriveClock / frameSize; - long dPerSecond = (long)(frameSize * dRate); - DriveCyclesPerMs = dPerSecond / 1000; - - long TStatesPerDriveCycle = (long)(_machine.ULADevice.ClockSpeed / DriveClock); - StatesPerDriveTick = TStatesPerDriveCycle; - } - - /// - /// Called by reads to the main status register - /// Returns true if there is no delay - /// Returns false if read is to be deferred - /// - private bool CheckTiming() - { - // get delta - long delta = CurrentCPUCycle - LastCPUCycle; - - if (StatusDelay >= delta) - { - // there is still delay remaining - StatusDelay -= delta; - LastCPUCycle = CurrentCPUCycle; - return false; - } - else - { - // no delay remaining - StatusDelay = 0; - LastCPUCycle = CurrentCPUCycle; - return true; - } - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.cs deleted file mode 100644 index 1339c5524b2..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPD765.cs +++ /dev/null @@ -1,232 +0,0 @@ -using BizHawk.Common; -using System.Collections.Generic; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// The NEC floppy disk controller (and floppy drive) found in the +3 - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 - { - /// - /// The emulated spectrum machine - /// - private SpectrumBase _machine; - - /// - /// Main constructor - /// - public NECUPD765() - { - InitCommandList(); - } - - /// - /// Initialization routine - /// - public void Init(SpectrumBase machine) - { - _machine = machine; - FDD_Init(); - TimingInit(); - Reset(); - } - - /// - /// Resets the FDC - /// - public void Reset() - { - // setup main status - StatusMain = 0; - - Status0 = 0; - Status1 = 0; - Status2 = 0; - Status3 = 0; - - SetBit(MSR_RQM, ref StatusMain); - - SetPhase_Idle(); - - //FDC_FLAG_RQM = true; - //ActiveDirection = CommandDirection.IN; - SRT = 6; - HUT = 16; - HLT = 2; - HLT_Counter = 0; - HUT_Counter = 0; - IndexPulseCounter = 0; - CMD_FLAG_MF = false; - - foreach (var d in DriveStates) - { - //d.SeekingTrack = d.CurrentTrack; - ////d.SeekCounter = 0; - //d.FLAG_SEEK_INTERRUPT = false; - //d.IntStatus = 0; - //d.SeekState = SeekSubState.Idle; - //d.SeekIntState = SeekIntStatus.Normal; - } - } - - /// - /// Setup the command structure - /// Each command represents one of the internal UPD765 commands - /// - private void InitCommandList() - { - CommandList = new List - { - // read data - new Command { CommandDelegate = UPD_ReadData, CommandCode = 0x06, MT = true, MF = true, SK = true, IsRead = true, - Direction = CommandDirection.OUT, ParameterByteCount = 8, ResultByteCount = 7 }, - // read id - new Command { CommandDelegate = UPD_ReadID, CommandCode = 0x0a, MF = true, IsRead = true, - Direction = CommandDirection.OUT, ParameterByteCount = 1, ResultByteCount = 7 }, - // specify - new Command { CommandDelegate = UPD_Specify, CommandCode = 0x03, - Direction = CommandDirection.OUT, ParameterByteCount = 2, ResultByteCount = 0 }, - // read diagnostic - new Command { CommandDelegate = UPD_ReadDiagnostic, CommandCode = 0x02, MF = true, SK = true, IsRead = true, - Direction = CommandDirection.OUT, ParameterByteCount = 8, ResultByteCount = 7 }, - // scan equal - new Command { CommandDelegate = UPD_ScanEqual, CommandCode = 0x11, MT = true, MF = true, SK = true, IsRead = true, - Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, - // scan high or equal - new Command { CommandDelegate = UPD_ScanHighOrEqual, CommandCode = 0x1d, MT = true, MF = true, SK = true, IsRead = true, - Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, - // scan low or equal - new Command { CommandDelegate = UPD_ScanLowOrEqual, CommandCode = 0x19, MT = true, MF = true, SK = true, IsRead = true, - Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, - // read deleted data - new Command { CommandDelegate = UPD_ReadDeletedData, CommandCode = 0x0c, MT = true, MF = true, SK = true, IsRead = true, - Direction = CommandDirection.OUT, ParameterByteCount = 8, ResultByteCount = 7 }, - // write data - new Command { CommandDelegate = UPD_WriteData, CommandCode = 0x05, MT = true, MF = true, IsWrite = true, - Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, - // write id - new Command { CommandDelegate = UPD_WriteID, CommandCode = 0x0d, MF = true, IsWrite = true, - Direction = CommandDirection.IN, ParameterByteCount = 5, ResultByteCount = 7 }, - // write deleted data - new Command { CommandDelegate = UPD_WriteDeletedData, CommandCode = 0x09, MT = true, MF = true, IsWrite = true, - Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, - // seek - new Command { CommandDelegate = UPD_Seek, CommandCode = 0x0f, - Direction = CommandDirection.OUT, ParameterByteCount = 2, ResultByteCount = 0 }, - // recalibrate (seek track00) - new Command { CommandDelegate = UPD_Recalibrate, CommandCode = 0x07, - Direction = CommandDirection.OUT, ParameterByteCount = 1, ResultByteCount = 0 }, - // sense interrupt status - new Command { CommandDelegate = UPD_SenseInterruptStatus, CommandCode = 0x08, - Direction = CommandDirection.OUT, ParameterByteCount = 0, ResultByteCount = 2 }, - // sense drive status - new Command { CommandDelegate = UPD_SenseDriveStatus, CommandCode = 0x04, - Direction = CommandDirection.OUT, ParameterByteCount = 1, ResultByteCount = 1 }, - // version - new Command { CommandDelegate = UPD_Version, CommandCode = 0x10, - Direction = CommandDirection.OUT, ParameterByteCount = 0, ResultByteCount = 1 }, - // invalid - new Command { CommandDelegate = UPD_Invalid, CommandCode = 0x00, - Direction = CommandDirection.OUT, ParameterByteCount = 0, ResultByteCount = 1 }, - }; - } - - public void SyncState(Serializer ser) - { - void SyncFDDState(Serializer ser1) - { - ser1.Sync(nameof(FDD_FLAG_MOTOR), ref FDD_FLAG_MOTOR); - - for (int i = 0; i < 4; i++) - { - ser1.BeginSection("HITDrive_" + i); - DriveStates[i].SyncState(ser1); - ser1.EndSection(); - } - - ser1.Sync(nameof(DiskDriveIndex), ref _diskDriveIndex); - // set active drive - DiskDriveIndex = _diskDriveIndex; - } - - void SyncRegisterState(Serializer ser1) - { - ser1.Sync("_RegMain", ref StatusMain); - ser1.Sync("_Reg0", ref Status0); - ser1.Sync("_Reg1", ref Status1); - ser1.Sync("_Reg2", ref Status2); - ser1.Sync("_Reg3", ref Status3); - } - - void SyncControllerState(Serializer ser1) - { - ser1.Sync(nameof(DriveLight), ref DriveLight); - ser1.SyncEnum(nameof(ActivePhase), ref ActivePhase); -#if false - ser1.SyncEnum(nameof(ActiveDirection), ref ActiveDirection); -#endif - ser1.SyncEnum(nameof(ActiveInterrupt), ref ActiveInterrupt); - ser1.Sync(nameof(CommBuffer), ref CommBuffer, false); - ser1.Sync(nameof(CommCounter), ref CommCounter); - ser1.Sync(nameof(ResBuffer), ref ResBuffer, false); - ser1.Sync(nameof(ExecBuffer), ref ExecBuffer, false); - ser1.Sync(nameof(ExecCounter), ref ExecCounter); - ser1.Sync(nameof(ExecLength), ref ExecLength); - ser1.Sync(nameof(InterruptResultBuffer), ref InterruptResultBuffer, false); - ser1.Sync(nameof(ResCounter), ref ResCounter); - ser1.Sync(nameof(ResLength), ref ResLength); - ser1.Sync(nameof(LastSectorDataWriteByte), ref LastSectorDataWriteByte); - ser1.Sync(nameof(LastSectorDataReadByte), ref LastSectorDataReadByte); - ser1.Sync(nameof(LastByteReceived), ref LastByteReceived); - - ser1.Sync(nameof(_cmdIndex), ref _cmdIndex); - // resync the ActiveCommand - CMDIndex = _cmdIndex; - - ActiveCommandParams.SyncState(ser1); - - ser1.Sync(nameof(IndexPulseCounter), ref IndexPulseCounter); -#if false - ser1.SyncEnum(nameof(_activeStatus), ref _activeStatus); - ser1.SyncEnum(nameof(_statusRaised), ref _statusRaised); -#endif - - ser1.Sync(nameof(CMD_FLAG_MT), ref CMD_FLAG_MT); - ser1.Sync(nameof(CMD_FLAG_MF), ref CMD_FLAG_MF); - ser1.Sync(nameof(CMD_FLAG_SK), ref CMD_FLAG_SK); - ser1.Sync(nameof(SRT), ref SRT); - ser1.Sync(nameof(HUT), ref HUT); - ser1.Sync(nameof(HLT), ref HLT); - ser1.Sync(nameof(ND), ref ND); - ser1.Sync(nameof(SRT_Counter), ref SRT_Counter); - ser1.Sync(nameof(HUT_Counter), ref HUT_Counter); - ser1.Sync(nameof(HLT_Counter), ref HLT_Counter); - - ser1.Sync(nameof(SectorDelayCounter), ref SectorDelayCounter); - ser1.Sync(nameof(SectorID), ref SectorID); - } - - void SyncTimingState(Serializer ser1) - { - ser1.Sync(nameof(LastCPUCycle), ref LastCPUCycle); - ser1.Sync(nameof(StatusDelay), ref StatusDelay); - ser1.Sync(nameof(TickCounter), ref TickCounter); - ser1.Sync(nameof(DriveCycleCounter), ref DriveCycleCounter); - } - - ser.BeginSection("NEC-UPD765"); - SyncFDDState(ser); - SyncRegisterState(ser); - SyncControllerState(ser); - SyncTimingState(ser); - ser.EndSection(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPS765.Static.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPS765.Static.cs deleted file mode 100644 index 1d638fc8d21..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/NECUPS765.Static.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Collections; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Static helper methods - /// - /* - Implementation based on the information contained here: - http://www.cpcwiki.eu/index.php/765_FDC - and here: - http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf - */ - public partial class NECUPD765 - { - /// - /// Returns the specified bit value from supplied byte - /// - public static bool GetBit(int bitNumber, byte dataByte) - { - if (bitNumber < 0 || bitNumber > 7) - return false; - - BitArray bi = new BitArray(new byte[] { dataByte }); - - return bi[bitNumber]; - } - - /// - /// Sets the specified bit of the supplied byte to 1 - /// - public static void SetBit(int bitNumber, ref byte dataByte) - { - if (bitNumber < 0 || bitNumber > 7) - return; - - int db = dataByte; - - db |= 1 << bitNumber; - - dataByte = (byte)db; - } - - /// - /// Sets the specified bit of the supplied byte to 0 - /// - public static void UnSetBit(int bitNumber, ref byte dataByte) - { - if (bitNumber < 0 || bitNumber > 7) - return; - - int db = dataByte; - - db &= ~(1 << bitNumber); - - dataByte = (byte)db; - } - - /// - /// Returns a drive number (0-3) based on the first two bits of the supplied byte - /// - public static int GetUnitSelect(byte dataByte) - { - int driveNumber = dataByte & 0x03; - return driveNumber; - } - - /// - /// Sets the first two bits of a byte based on the supplied drive number (0-3) - /// - public static void SetUnitSelect(int driveNumber, ref byte dataByte) - { - switch (driveNumber) - { - case 0: - UnSetBit(SR0_US0, ref dataByte); - UnSetBit(SR0_US1, ref dataByte); - break; - case 1: - SetBit(SR0_US0, ref dataByte); - UnSetBit(SR0_US1, ref dataByte); - break; - case 2: - SetBit(SR0_US1, ref dataByte); - UnSetBit(SR0_US0, ref dataByte); - break; - case 3: - SetBit(SR0_US0, ref dataByte); - SetBit(SR0_US1, ref dataByte); - break; - } - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/Plus3DosDirectory.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/Plus3DosDirectory.cs new file mode 100644 index 00000000000..81df0ee1604 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/Plus3DosDirectory.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Text; + +using BizHawk.Emulation.Cores.Floppy; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Reads the +3DOS (CP/M 2.2-style) directory off a +3 disk's flux, listing the files it contains. Useful + /// for diagnostics / OSD - e.g. to see what to LOAD when a disk is a data disk rather than a self-booting + /// one, or to confirm a disk decoded into a usable filesystem. Works on the shared FluxDisk + /// (side 0), reading the +3 disk specification from logical sector 0 when present, otherwise assuming the + /// standard 180K +3 DATA format. This reads the filesystem only; it does not interpret copy protection. + /// + public static class Plus3DosDirectory + { + public sealed class Entry + { + public int UserNumber; + public string Name; // "NAME.EXT" (or just "NAME") + public long SizeBytes; // sum of the records across the file's extents * 128 + public bool ReadOnly; + public bool SystemHidden; + public bool Archived; + + public override string ToString() + { + var attr = (ReadOnly ? "R" : "-") + (SystemHidden ? "S" : "-") + (Archived ? "A" : "-"); + return $"{Name,-13} {SizeBytes,7} {attr}" + (UserNumber == 0 ? "" : $" (user {UserNumber})"); + } + } + + /// + /// Read the directory from side 0 of the disk. Returns an empty list if no valid +3DOS + /// directory is present (e.g. a custom-formatted / non-filesystem disk). + /// + public static List Read(FluxDisk disk) + { + var files = new List(); + if (disk == null) return files; + + // figure out where the directory lives: read the +3 disk spec from logical sector 0 if it looks + // valid, otherwise fall back to the standard 180K +3 DATA format (0 reserved tracks, 64 entries). + int reservedTracks = 0, dirEntries = 64; + var spec = LogicalSector0(disk); + if (spec != null && spec.Length >= 10 && spec[0] <= 3 + && (spec[2] == 40 || spec[2] == 80) && spec[4] == 2 && spec[6] == 3) + { + reservedTracks = spec[5]; + int blockSize = 128 << spec[6]; + dirEntries = spec[7] * blockSize / 32; + } + + // the directory occupies the first (dirEntries*32) bytes of the first track after the reserved ones, + // taken from that track's sectors in logical (ascending R) order + byte[] dir = ReadTrackBytes(disk, reservedTracks, dirEntries * 32); + if (dir == null) return files; + + // accumulate file size across extents (CP/M splits large files into 32-byte extent records) + var byName = new Dictionary(); + for (int off = 0; off + 32 <= dir.Length; off += 32) + { + int user = dir[off]; + if (user > 15) continue; // 0xE5 = empty/deleted, or a non-file (label/timestamp) entry + + var sb = new StringBuilder(12); + for (int i = 1; i <= 8; i++) { char c = (char)(dir[off + i] & 0x7F); if (c != ' ') sb.Append(c); } + int nameLen = sb.Length; + string ext = ""; + { + var e = new StringBuilder(3); + for (int i = 9; i <= 11; i++) { char c = (char)(dir[off + i] & 0x7F); if (c != ' ') e.Append(c); } + ext = e.ToString(); + } + if (nameLen == 0 && ext.Length == 0) continue; + if (!LooksLikeFilename(sb.ToString(), ext)) continue; + + string full = ext.Length > 0 ? sb.ToString() + "." + ext : sb.ToString(); + int records = dir[off + 15]; + + if (!byName.TryGetValue(full, out var entry)) + { + entry = new Entry + { + UserNumber = user, + Name = full, + ReadOnly = (dir[off + 9] & 0x80) != 0, + SystemHidden = (dir[off + 10] & 0x80) != 0, + Archived = (dir[off + 11] & 0x80) != 0, + }; + byName[full] = entry; + files.Add(entry); + } + // CP/M splits a file across 32-byte extent entries; total size = sum of every extent's records * 128 + entry.SizeBytes += records * 128L; + } + return files; + } + + /// + /// Convenience: a human-readable multi-line catalogue (empty string if no files). + /// + public static string Catalogue(FluxDisk disk) + { + var files = Read(disk); + if (files.Count == 0) return ""; + var sb = new StringBuilder(); + foreach (var f in files) sb.AppendLine(f.ToString()); + return sb.ToString(); + } + + // logical sector 0 = the lowest-numbered sector on track 0 (its data is the +3 disk specification) + private static byte[] LogicalSector0(FluxDisk disk) + { + var t = disk.GetTrack(0, 0); + if (t == null) return null; + var secs = StandardMfmFormat.DecodeSectors(t); + byte[] best = null; int bestR = int.MaxValue; + foreach (var s in secs) + if (s.HasData && s.R < bestR) { bestR = s.R; best = s.Data; } + return best; + } + + // concatenate a track's sectors in ascending-R (logical) order, up to byteCount bytes + private static byte[] ReadTrackBytes(FluxDisk disk, int cyl, int byteCount) + { + var t = disk.GetTrack(cyl, 0); + if (t == null) return null; + var secs = StandardMfmFormat.DecodeSectors(t); + secs.Sort((a, b) => a.R.CompareTo(b.R)); + var buf = new byte[byteCount]; + int pos = 0; + foreach (var s in secs) + { + if (pos >= byteCount) break; + if (!s.HasData) continue; + int n = System.Math.Min(s.Data.Length, byteCount - pos); + System.Array.Copy(s.Data, 0, buf, pos, n); + pos += n; + } + return pos == 0 ? null : buf; + } + + private static bool LooksLikeFilename(string name, string ext) + { + foreach (var c in name) if (c < 0x20 || c > 0x7E) return false; + foreach (var c in ext) if (c < 0x20 || c > 0x7E) return false; + return true; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/Upd765DiskController.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/Upd765DiskController.cs new file mode 100644 index 00000000000..053e8ec3b48 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Disk/Upd765DiskController.cs @@ -0,0 +1,100 @@ +using BizHawk.Common; +using BizHawk.Emulation.Cores.Floppy; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The +3's floppy controller, backed by the shared flux subsystem: a timed uPD765 controller + /// driving an EME-156 single-head 3" drive over a flux/cell disk model. Disk images (DSK/EDSK/IPF/HFE/ + /// SCP/FDI/raw) are converted to flux on load. The controller is timed, but the +3's INT pin is not wired + /// to the Z80 (software polls the main status register), so it is advanced lazily on each register access + /// using the CPU cycle count - which naturally keeps the byte cadence in step with the +3DOS poll loop. + /// + public sealed class Upd765DiskController : IFloppyDiskController + { + private SpectrumBase _machine; + private Upd765Fdc _fdc = new(); + private Eme156Drive _drive = new(); + private long _lastCycle; + private bool _diskLoaded; + private DiskProtectionScheme _protection = DiskProtectionScheme.None; + + public bool FDD_FLAG_MOTOR { get; set; } + + public void Init(SpectrumBase machine) + { + _machine = machine; + _fdc.Drives[0] = _drive; + _fdc.ConfigureTiming(machine.ULADevice.ClockSpeed); + _fdc.Reset(); + _lastCycle = Cycles; + } + + private long Cycles => _machine?.CPU.TotalExecutedCycles ?? 0; + + // Advance the controller to "now" (the +3 polls, so this runs on every FDC register access). + private void CatchUp() + { + long delta = Cycles - _lastCycle; + _lastCycle = Cycles; + while (delta > 0) + { + int step = (int)System.Math.Min(delta, 100_000); + _fdc.Clock(step); + delta -= step; + } + } + + public bool ReadPort(ushort port, ref int data) + { + if (port == 0x3ffd) { CatchUp(); data = _fdc.ReadData(); return true; } + if (port == 0x2ffd) { CatchUp(); data = _fdc.ReadStatus(); return true; } + return false; + } + + public bool WritePort(ushort port, int data) + { + if (port == 0x3ffd) { CatchUp(); _fdc.WriteData((byte)data); return true; } + if (port == 0x1ffd) { CatchUp(); FDD_FLAG_MOTOR = (data & 0x08) != 0; _drive.MotorOn = FDD_FLAG_MOTOR; return true; } + return false; + } + + public void FDD_LoadDisk(byte[] diskData, int side) + { + var flux = DiskImageLoader.ToFluxDisk(diskData); + if (side >= 0 && flux.Sides > 1) flux = flux.ExtractSide(side); // present one side of a DS image + _drive.Disk = flux; + _diskLoaded = _drive.Disk != null; + _protection = DiskProtection.Detect(_drive.Disk); + } + + public void FDD_EjectDisk() + { + _drive.Disk = null; + _diskLoaded = false; + _protection = DiskProtectionScheme.None; + } + + public bool FDD_IsDiskLoaded => _drive.Disk != null; + public bool DiskInserted => _drive.Disk != null; + public bool DriveLight => FDD_FLAG_MOTOR && _fdc.Active; + + /// + /// The detected copy-protection scheme, for the disk status display. + /// + public string ProtectionName => DiskProtection.DisplayName(_protection); + + public void SyncState(Serializer ser) + { + ser.BeginSection("Upd765DiskController"); + bool motor = FDD_FLAG_MOTOR; + ser.Sync(nameof(FDD_FLAG_MOTOR), ref motor); + FDD_FLAG_MOTOR = motor; + ser.Sync(nameof(_lastCycle), ref _lastCycle); + ser.Sync(nameof(_diskLoaded), ref _diskLoaded); + _fdc.SyncState(ser); + _drive.SyncState(ser); + ser.EndSection(); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AY38912.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AY38912.cs index 5b400487835..0725ce2e008 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AY38912.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AY38912.cs @@ -1,19 +1,17 @@ -using BizHawk.Common; +using BizHawk.Common; using BizHawk.Common.NumberExtensions; using BizHawk.Emulation.Common; - -using System.Collections.Generic; +using BizHawk.Emulation.Cores.Components; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// - /// AY-3-8912 Emulated Device - /// - /// Based heavily on the YM-2149F / AY-3-8910 emulator used in Unreal Speccy - /// (Originally created under Public Domain license by SMT jan.2006) + /// AY-3-8912 Emulated Device (ZX Spectrum adapter) /// - /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.cpp - /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.h + /// This is now a THIN ADAPTER around the shared DSP core. + /// It provides the ZX-bus-specific port decode (0xFFFD / 0xBFFD) and the ZX + /// settings glue (pan config / volume), delegating all sound generation and + /// state to the shared core. /// public class AY38912 : IPSG { @@ -22,13 +20,10 @@ public class AY38912 : IPSG /// private readonly SpectrumBase _machine; - private int _tStatesPerFrame; - private int _sampleRate; - private int _samplesPerFrame; - private int _tStatesPerSample; - private short[] _audioBuffer; - private int _audioBufferIndex; - private int _lastStateRendered; + /// + /// The shared PSG DSP core + /// + private readonly AY391x _core = new AY391x(); /// /// Main constructor @@ -43,9 +38,11 @@ public AY38912(SpectrumBase machine) /// public void Init(int sampleRate, int tStatesPerFrame) { - InitTiming(sampleRate, tStatesPerFrame); - UpdateVolume(); - Reset(); + // The Spectrum AY runs at CPU/2; tStatesPerFrame is measured in CPU T-states. Passing the actual + // per-model CPU clock makes the AY pitch correct for every model (incl. Pentagon, whose AY is + // 1.792 MHz, not the 1.7734 MHz the old build hard-coded). + int cpuClock = _machine.ULADevice.ClockSpeed; + _core.Init(sampleRate, tStatesPerFrame, cpuClock / 2, cpuClock); } /// @@ -58,7 +55,7 @@ public bool ReadPort(ushort port, ref int value) if ((port >> 14) == 3) { // port read is addressing this device - value = PortRead(); + value = _core.PortRead(); return true; } } @@ -78,18 +75,17 @@ public bool WritePort(ushort port, int value) if ((port >> 14) == 3) { // register select - SelectedRegister = value & 0x0f; + _core.SelectedRegister = value & 0x0f; return true; } else if ((port >> 14) == 2) { // Update the audiobuffer based on the current CPU cycle // (this process the previous data BEFORE writing to the currently selected register) - int d = (int)(_machine.CurrentFrameCycle); - BufferUpdate(d); + _core.UpdateSound((int)_machine.CurrentFrameCycle); // write to register - PortWrite(value); + _core.PortWrite(value); return true; } } @@ -116,15 +112,8 @@ public enum AYPanConfig /// public AYPanConfig PanningConfiguration { - get => _currentPanTab; - set - { - if (value != _currentPanTab) - { - _currentPanTab = value; - UpdateVolume(); - } - } + get => (AYPanConfig)(int)_core.PanningConfiguration; + set => _core.PanningConfiguration = (AY391x.PanConfig)(int)value; } /// @@ -133,18 +122,8 @@ public AYPanConfig PanningConfiguration /// public int Volume { - get => _volume; - set - { - //value = Math.Max(0, value); - //value = Math.Max(100, value); - if (_volume == value) - { - return; - } - _volume = value; - UpdateVolume(); - } + get => _core.Volume; + set => _core.Volume = value; } /// @@ -152,8 +131,8 @@ public int Volume /// public int SelectedRegister { - get => _activeRegister; - set => _activeRegister = (byte)value; + get => _core.SelectedRegister; + set => _core.SelectedRegister = value; } /// @@ -161,7 +140,7 @@ public int SelectedRegister /// public int[] ExportRegisters() { - return _registers; + return _core.ExportRegisters(); } /// @@ -169,39 +148,7 @@ public int[] ExportRegisters() /// public void Reset() { - for (int i = 0; i < 16; i++) - { - if (i == 6) - _registers[i] = 0xff; - else - _registers[i] = 0; - } - - /* - _noiseVal = 0x0FFFF; - _outABC = 0; - _outNoiseABC = 0; - _counterNoise = 0; - _counterA = 0; - _counterB = 0; - _counterC = 0; - _EnvelopeCounterBend = 0; - - // clear all the registers - for (int i = 0; i < 14; i++) - { - SelectedRegister = i; - PortWrite(0); - } - - randomSeed = 1; - - // number of frames to update - var fr = (_audioBufferIndex * _tStatesPerFrame) / _audioBuffer.Length; - - // update the audio buffer - BufferUpdate(fr); - */ + _core.Reset(); } /// @@ -209,7 +156,7 @@ public void Reset() /// public int PortRead() { - return _registers[_activeRegister]; + return _core.PortRead(); } /// @@ -217,91 +164,7 @@ public int PortRead() /// public void PortWrite(int value) { - if (_activeRegister >= 0x10) - return; - - byte val = (byte)value; - if (_activeRegister is 1 or 3 or 5 or 13) val &= 0x0F; - else if (_activeRegister is 6 or 8 or 9 or 10) val &= 0x1F; - if (_activeRegister != 13 && _registers[_activeRegister] == val) - return; - - _registers[_activeRegister] = val; - - switch (_activeRegister) - { - // Channel A (Combined Pitch) - // (not written to directly) - case 0: - case 1: - _dividerA = _registers[AY_A_FINE] | (_registers[AY_A_COARSE] << 8); - break; - // Channel B (Combined Pitch) - // (not written to directly) - case 2: - case 3: - _dividerB = _registers[AY_B_FINE] | (_registers[AY_B_COARSE] << 8); - break; - // Channel C (Combined Pitch) - // (not written to directly) - case 4: - case 5: - _dividerC = _registers[AY_C_FINE] | (_registers[AY_C_COARSE] << 8); - break; - // Noise Pitch - case 6: - _dividerN = val * 2; - break; - // Mixer - case 7: - _bit0 = 0 - ((val >> 0) & 1); - _bit1 = 0 - ((val >> 1) & 1); - _bit2 = 0 - ((val >> 2) & 1); - _bit3 = 0 - ((val >> 3) & 1); - _bit4 = 0 - ((val >> 4) & 1); - _bit5 = 0 - ((val >> 5) & 1); - break; - // Channel Volumes - case 8: - _eMaskA = (val & 0x10) != 0 ? -1 : 0; - _vA = ((val & 0x0F) * 2 + 1) & ~_eMaskA; - break; - case 9: - _eMaskB = (val & 0x10) != 0 ? -1 : 0; - _vB = ((val & 0x0F) * 2 + 1) & ~_eMaskB; - break; - case 10: - _eMaskC = (val & 0x10) != 0 ? -1 : 0; - _vC = ((val & 0x0F) * 2 + 1) & ~_eMaskC; - break; - // Envelope (Combined Duration) - // (not written to directly) - case 11: - case 12: - _dividerE = _registers[AY_E_FINE] | (_registers[AY_E_COARSE] << 8); - break; - // Envelope Shape - case 13: - // reset the envelope counter - _countE = 0; - - if ((_registers[AY_E_SHAPE] & 4) != 0) - { - // attack - _eState = 0; - _eDirection = 1; - } - else - { - // decay - _eState = 31; - _eDirection = -1; - } - break; - case 14: - // IO Port - not implemented - break; - } + _core.PortWrite(value); } /// @@ -309,8 +172,7 @@ public void PortWrite(int value) /// public void StartFrame() { - _audioBufferIndex = 0; - BufferUpdate(0); + _core.StartFrame(); } /// @@ -318,7 +180,7 @@ public void StartFrame() /// public void EndFrame() { - BufferUpdate(_tStatesPerFrame); + _core.EndFrame(); } /// @@ -326,448 +188,51 @@ public void EndFrame() /// public void UpdateSound(int frameCycle) { - BufferUpdate(frameCycle); - } - - /// - /// Register indicies - /// - private const int AY_A_FINE = 0; - private const int AY_A_COARSE = 1; - private const int AY_B_FINE = 2; - private const int AY_B_COARSE = 3; - private const int AY_C_FINE = 4; - private const int AY_C_COARSE = 5; - private const int AY_NOISEPITCH = 6; - private const int AY_MIXER = 7; - private const int AY_A_VOL = 8; - private const int AY_B_VOL = 9; - private const int AY_C_VOL = 10; - private const int AY_E_FINE = 11; - private const int AY_E_COARSE = 12; - private const int AY_E_SHAPE = 13; - private const int AY_PORT_A = 14; - private const int AY_PORT_B = 15; - - /// - /// The register array - /// - /* - The AY-3-8910/8912 contains 16 internal registers as follows: - - Register Function Range - 0 Channel A fine pitch 8-bit (0-255) - 1 Channel A course pitch 4-bit (0-15) - 2 Channel B fine pitch 8-bit (0-255) - 3 Channel B course pitch 4-bit (0-15) - 4 Channel C fine pitch 8-bit (0-255) - 5 Channel C course pitch 4-bit (0-15) - 6 Noise pitch 5-bit (0-31) - 7 Mixer 8-bit (see below) - 8 Channel A volume 4-bit (0-15, see below) - 9 Channel B volume 4-bit (0-15, see below) - 10 Channel C volume 4-bit (0-15, see below) - 11 Envelope fine duration 8-bit (0-255) - 12 Envelope course duration 8-bit (0-255) - 13 Envelope shape 4-bit (0-15) - 14 I/O port A 8-bit (0-255) - 15 I/O port B 8-bit (0-255) (Not present on the AY-3-8912) - - * The volume registers (8, 9 and 10) contain a 4-bit setting but if bit 5 is set then that channel uses the - envelope defined by register 13 and ignores its volume setting. - * The mixer (register 7) is made up of the following bits (low=enabled): - - Bit: 7 6 5 4 3 2 1 0 - Register: I/O I/O Noise Noise Noise Tone Tone Tone - Channel: B A C B A C B A - - The AY-3-8912 ignores bit 7 of this register. - */ - private int[] _registers = new int[16]; - - /// - /// The currently selected register - /// - private byte _activeRegister; - - /// - /// The frequency of the AY chip - /// - private static readonly int _chipFrequency = 1773400; - - /// - /// The rendering resolution of the chip - /// - private double _resolution = 50D * 8D / _chipFrequency; - - /// - /// Channel generator state - /// - private int _bitA; - private int _bitB; - private int _bitC; - - /// - /// Envelope state - /// - private int _eState; - - /// - /// Envelope direction - /// - private int _eDirection; - - /// - /// Noise seed - /// - private int _noiseSeed; - - /// - /// Mixer state - /// - private int _bit0; - private int _bit1; - private int _bit2; - private int _bit3; - private int _bit4; - private int _bit5; - - /// - /// Noise generator state - /// - private int _bitN; - - /// - /// Envelope masks - /// - private int _eMaskA; - private int _eMaskB; - private int _eMaskC; - - /// - /// Amplitudes - /// - private int _vA; - private int _vB; - private int _vC; - - /// - /// Channel gen counters - /// - private int _countA; - private int _countB; - private int _countC; - - /// - /// Envelope gen counter - /// - private int _countE; - - /// - /// Noise gen counter - /// - private int _countN; - - /// - /// Channel gen dividers - /// - private int _dividerA; - private int _dividerB; - private int _dividerC; - - /// - /// Envelope gen divider - /// - private int _dividerE; - - /// - /// Noise gen divider - /// - private int _dividerN; - - /// - /// Panning table list - /// - private static readonly List PanTabs = new List - { - // MONO - new uint[] { 50,50, 50,50, 50,50 }, - // ABC - new uint[] { 100,10, 66,66, 10,100 }, - // ACB - new uint[] { 100,10, 10,100, 66,66 }, - // BAC - new uint[] { 66,66, 100,10, 10,100 }, - // BCA - new uint[] { 10,100, 100,10, 66,66 }, - // CAB - new uint[] { 66,66, 10,100, 100,10 }, - // CBA - new uint[] { 10,100, 66,66, 100,10 } - }; - - /// - /// The currently selected panning configuration - /// - private AYPanConfig _currentPanTab = AYPanConfig.ABC; - - /// - /// The current volume - /// - private int _volume = 75; - - /// - /// Volume tables state - /// - private uint[][] _volumeTables; - - /// - /// Volume table to be used - /// - private static readonly uint[] AYVolumes = - { - 0x0000,0x0000,0x0340,0x0340,0x04C0,0x04C0,0x06F2,0x06F2, - 0x0A44,0x0A44,0x0F13,0x0F13,0x1510,0x1510,0x227E,0x227E, - 0x289F,0x289F,0x414E,0x414E,0x5B21,0x5B21,0x7258,0x7258, - 0x905E,0x905E,0xB550,0xB550,0xD7A0,0xD7A0,0xFFFF,0xFFFF, - }; - - /// - /// Forces an update of the volume tables - /// - private void UpdateVolume() - { - int upperFloor = 40000; - var inc = (0xFFFF - upperFloor) / 100; - - var vol = inc * _volume; // ((ulong)0xFFFF * (ulong)_volume / 100UL) - 20000 ; - _volumeTables = new uint[6][]; - - // parent array - for (int j = 0; j < _volumeTables.Length; j++) - { - _volumeTables[j] = new uint[32]; - - // child array - for (int i = 0; i < _volumeTables[j].Length; i++) - { - _volumeTables[j][i] = (uint)( - (PanTabs[(int)_currentPanTab][j] * AYVolumes[i] * vol) / - (3 * 65535 * 100)); - } - } + _core.UpdateSound(frameCycle); } - private int mult_const; - - /// - /// Initializes timing information for the frame - /// - private void InitTiming(int sampleRate, int frameTactCount) - { - _sampleRate = sampleRate; - _tStatesPerFrame = frameTactCount; - _samplesPerFrame = 882; - - _tStatesPerSample = 79; //(int)Math.Round(((double)_tStatesPerFrame * 50D) / - //(16D * (double)_sampleRate), - //MidpointRounding.AwayFromZero); - - //_samplesPerFrame = _tStatesPerFrame / _tStatesPerSample; - _audioBuffer = new short[_samplesPerFrame * 2]; //[_sampleRate / 50]; - _audioBufferIndex = 0; + public bool CanProvideAsync => _core.CanProvideAsync; - mult_const = ((_chipFrequency / 8) << 14) / _machine.ULADevice.ClockSpeed; - - var aytickspercputick = _machine.ULADevice.ClockSpeed / (double)_chipFrequency; - int ayCyclesPerSample = (int)(_tStatesPerSample * (double)aytickspercputick); - } - - /// - /// Updates the audiobuffer based on the current frame t-state - /// - private void BufferUpdate(int cycle) - { - if (cycle > _tStatesPerFrame) - { - // we are outside of the frame - just process the last value - cycle = _tStatesPerFrame; - } - - // get the current length of the audiobuffer - int bufferLength = _samplesPerFrame; // _audioBuffer.Length; - - int toEnd = ((bufferLength * cycle) / _tStatesPerFrame); - - // loop through the number of samples we need to render - while (_audioBufferIndex < toEnd) - { - // run the AY chip processing at the correct resolution - for (int i = 0; i < _tStatesPerSample / 14; i++) - { - if (++_countA >= _dividerA) - { - _countA = 0; - _bitA ^= -1; - } - - if (++_countB >= _dividerB) - { - _countB = 0; - _bitB ^= -1; - } - - if (++_countC >= _dividerC) - { - _countC = 0; - _bitC ^= -1; - } - - if (++_countN >= _dividerN) - { - _countN = 0; - _noiseSeed = (_noiseSeed * 2 + 1) ^ (((_noiseSeed >> 16) ^ (_noiseSeed >> 13)) & 1); - _bitN = 0 - ((_noiseSeed >> 16) & 1); - } - - if (++_countE >= _dividerE) - { - _countE = 0; - _eState += +_eDirection; - - if ((_eState & ~31) != 0) - { - var val = _registers[AY_E_SHAPE]; - if (val is <= 7 or 9 or 15) - { - _eState = _eDirection = 0; - } - else if (val is 8 or 12) - { - _eState &= 31; - } - else if (val is 10 or 14) - { - _eDirection = -_eDirection; - _eState += _eDirection; - } - else /*if (val is 11 or 13)*/ - { - _eState = 31; - _eDirection = 0; - } - } - } - } - - // mix the sample - var mixA = ((_eMaskA & _eState) | _vA) & ((_bitA | _bit0) & (_bitN | _bit3)); - var mixB = ((_eMaskB & _eState) | _vB) & ((_bitB | _bit1) & (_bitN | _bit4)); - var mixC = ((_eMaskC & _eState) | _vC) & ((_bitC | _bit2) & (_bitN | _bit5)); - - var l = _volumeTables[0][mixA]; - var r = _volumeTables[1][mixA]; - - l += _volumeTables[2][mixB]; - r += _volumeTables[3][mixB]; - l += _volumeTables[4][mixC]; - r += _volumeTables[5][mixC]; - - _audioBuffer[_audioBufferIndex * 2] = (short)l; - _audioBuffer[(_audioBufferIndex * 2) + 1] = (short)r; - - _audioBufferIndex++; - } - - _lastStateRendered = cycle; - } - - public bool CanProvideAsync => false; - - public SyncSoundMode SyncMode => SyncSoundMode.Sync; + public SyncSoundMode SyncMode => _core.SyncMode; public void SetSyncMode(SyncSoundMode mode) { - if (mode != SyncSoundMode.Sync) - throw new InvalidOperationException("Only Sync mode is supported."); + _core.SetSyncMode(mode); } public void GetSamplesAsync(short[] samples) { - throw new NotSupportedException("Async is not available"); + _core.GetSamplesAsync(samples); } public void DiscardSamples() { - _audioBuffer = new short[_samplesPerFrame * 2]; + _core.DiscardSamples(); } public void GetSamplesSync(out short[] samples, out int nsamp) { - nsamp = _samplesPerFrame; - samples = _audioBuffer; - DiscardSamples(); + _core.GetSamplesSync(out samples, out nsamp); } - public int nullDump = 0; - /// /// State serialization /// public void SyncState(Serializer ser) { - ser.BeginSection("PSG-AY"); - - ser.Sync(nameof(_tStatesPerFrame), ref _tStatesPerFrame); - ser.Sync(nameof(_sampleRate), ref _sampleRate); - ser.Sync(nameof(_samplesPerFrame), ref _samplesPerFrame); - ser.Sync(nameof(_tStatesPerSample), ref _tStatesPerSample); - ser.Sync(nameof(_audioBufferIndex), ref _audioBufferIndex); - ser.Sync(nameof(_audioBuffer), ref _audioBuffer, false); - - ser.Sync(nameof(_registers), ref _registers, false); - ser.Sync(nameof(_activeRegister), ref _activeRegister); - ser.Sync(nameof(_bitA), ref _bitA); - ser.Sync(nameof(_bitB), ref _bitB); - ser.Sync(nameof(_bitC), ref _bitC); - ser.Sync(nameof(_eState), ref _eState); - ser.Sync(nameof(_eDirection), ref _eDirection); - ser.Sync(nameof(_noiseSeed), ref _noiseSeed); - ser.Sync(nameof(_bit0), ref _bit0); - ser.Sync(nameof(_bit1), ref _bit1); - ser.Sync(nameof(_bit2), ref _bit2); - ser.Sync(nameof(_bit3), ref _bit3); - ser.Sync(nameof(_bit4), ref _bit4); - ser.Sync(nameof(_bit5), ref _bit5); - ser.Sync(nameof(_bitN), ref _bitN); - ser.Sync(nameof(_eMaskA), ref _eMaskA); - ser.Sync(nameof(_eMaskB), ref _eMaskB); - ser.Sync(nameof(_eMaskC), ref _eMaskC); - ser.Sync(nameof(_vA), ref _vA); - ser.Sync(nameof(_vB), ref _vB); - ser.Sync(nameof(_vC), ref _vC); - ser.Sync(nameof(_countA), ref _countA); - ser.Sync(nameof(_countB), ref _countB); - ser.Sync(nameof(_countC), ref _countC); - ser.Sync(nameof(_countE), ref _countE); - ser.Sync(nameof(_countN), ref _countN); - ser.Sync(nameof(_dividerA), ref _dividerA); - ser.Sync(nameof(_dividerB), ref _dividerB); - ser.Sync(nameof(_dividerC), ref _dividerC); - ser.Sync(nameof(_dividerE), ref _dividerE); - ser.Sync(nameof(_dividerN), ref _dividerN); - ser.SyncEnum(nameof(_currentPanTab), ref _currentPanTab); - ser.Sync(nameof(_volume), ref nullDump); - - for (int i = 0; i < 6; i++) - { - ser.Sync("volTable" + i, ref _volumeTables[i], false); - } + _core.SyncState(ser); if (ser.IsReader) - _volume = _machine.Spectrum.Settings.AYVolume; + _core.SetVolumeNoRebuild(_machine.Spectrum.Settings.AYVolume); + } - ser.EndSection(); + /// + /// Releases the shared PSG core (and its unmanaged BlipBuffers). Called from + /// ZXSpectrum.Dispose via the IPSG (: IDisposable) AYDevice. + /// + public void Dispose() + { + _core.Dispose(); } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/CPUMonitor.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/CPUMonitor.cs index b106a743d5d..529fe3abd4c 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/CPUMonitor.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/CPUMonitor.cs @@ -1,5 +1,5 @@ using BizHawk.Common; -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { @@ -10,7 +10,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum public class CPUMonitor { private readonly SpectrumBase _machine; - private readonly Z80A _cpu; + private readonly Z80AOpt _cpu; public MachineType machineType = MachineType.ZXSpectrum48; /// @@ -61,13 +61,23 @@ public void ExecuteCycle() _machine.ULADevice.CycleClock(TotalExecutedCycles); // is the next CPU cycle causing a BUSRQ or IORQ? - if (BUSRQ > 0) + // cache BUSRQ once: the getter does a machineType switch + array index, and it is otherwise + // re-read inside CheckIO and AscertainBUSRQAddress (2-3 reads/cycle) + ushort busrq = BUSRQ; + if (busrq > 0) { - // check for IORQ - if (!CheckIO()) + // check for IORQ. Only IO bus codes are >= BIO1 (100); every memory code is <= 21, and + // CheckIO matches nothing below BIO1 — so short-circuiting on the range skips its 8-case + // switch on the common memory cycle while staying exactly equivalent to `!CheckIO(busrq)`. + if (!(busrq >= Z80AOpt.BIO1 && CheckIO(busrq))) { - // is the memory address of the BUSRQ potentially contended? - if (_machine.IsContended(AscertainBUSRQAddress())) + // Is the memory address of the BUSRQ potentially contended? PageContended[addr >> 14] + // is a precomputed, non-virtual replacement for the per-cycle virtual IsContended(addr) + // (+ its paging switch on 128K/+2a). It is rebuilt from the paging state in + // RebuildMemoryMap and is value-identical to IsContended. GetContentionValue already + // reads the flat ContentionByCycle table. + ushort addr = AscertainBUSRQAddress(busrq); + if (_machine.PageContended[addr >> 14]) { var cont = _machine.ULADevice.GetContentionValue((int)_machine.CurrentFrameCycle); if (cont > 0) @@ -85,10 +95,10 @@ public void ExecuteCycle() /// /// Looks up the current BUSRQ address that is about to be signalled on the upcoming cycle /// - private ushort AscertainBUSRQAddress() + private ushort AscertainBUSRQAddress(ushort busrq) { ushort addr = 0; - switch (BUSRQ) + switch (busrq) { // PCh case 1: @@ -131,17 +141,17 @@ private ushort AscertainBUSRQAddress() addr = (ushort)(_cpu.Regs[_cpu.R] | _cpu.Regs[_cpu.I] << 8); break; // BC - case Z80A.BIO1: - case Z80A.BIO2: - case Z80A.BIO3: - case Z80A.BIO4: + case Z80AOpt.BIO1: + case Z80AOpt.BIO2: + case Z80AOpt.BIO3: + case Z80AOpt.BIO4: addr = (ushort)(_cpu.Regs[_cpu.C] | _cpu.Regs[_cpu.B] << 8); break; // WZ - case Z80A.WIO1: - case Z80A.WIO2: - case Z80A.WIO3: - case Z80A.WIO4: + case Z80AOpt.WIO1: + case Z80AOpt.WIO2: + case Z80AOpt.WIO3: + case Z80AOpt.WIO4: addr = (ushort)(_cpu.Regs[_cpu.Z] | _cpu.Regs[_cpu.W] << 8); break; } @@ -153,65 +163,65 @@ private ushort AscertainBUSRQAddress() /// Running every cycle, this determines whether the upcoming BUSRQ is for an IO operation /// Also processes any contention /// - private bool CheckIO() + private bool CheckIO(ushort busrq) { bool isIO = false; - switch (BUSRQ) + switch (busrq) { // BC: T1 - case Z80A.BIO1: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.BIO1: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(1)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // BC: T2 - case Z80A.BIO2: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.BIO2: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(2)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // BC: T3 - case Z80A.BIO3: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.BIO3: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(3)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // BC: T4 - case Z80A.BIO4: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.BIO4: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(4)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // WZ: T1 - case Z80A.WIO1: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.WIO1: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(1)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // WZ: T2 - case Z80A.WIO2: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.WIO2: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(2)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // WZ: T3 - case Z80A.WIO3: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.WIO3: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(3)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); break; // WZ: T4 - case Z80A.WIO4: - lastPortAddr = AscertainBUSRQAddress(); + case Z80AOpt.WIO4: + lastPortAddr = AscertainBUSRQAddress(busrq); isIO = true; if (IsIOCycleContended(4)) _cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle); @@ -360,12 +370,14 @@ private bool IsIOCycleContended(int T) } /// - /// Called when the first byte of an instruction is fetched + /// Called when the first byte of an instruction is fetched (M1), before the opcode byte is read. + /// The Pentagon uses this to drive the Beta 128 automatic TR-DOS ROM switch off the fetch address; + /// the enum check keeps every other model off the virtual dispatch on this per-fetch hot path. /// public void OnExecFetch(ushort firstByte) { - // fetch instruction without incrementing pc - //_cpu.FetchInstruction(_cpu.FetchMemory(firstByte)); + if (machineType == MachineType.Pentagon128) + _machine.TrapTrDos(firstByte); } public void SyncState(Serializer ser) diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Memory.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Memory.cs index 762d7d3d84d..2fc6dc2046d 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Memory.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Memory.cs @@ -52,7 +52,9 @@ public override byte ReadBus(ushort addr) case 0: TestForTapeTraps(addr % 0x4000); - if (ROMPaged == 0) + if (TRDOSPaged) + result = ROM2[addr % 0x4000]; + else if (ROMPaged == 0) result = ROM0[addr % 0x4000]; else result = ROM1[addr % 0x4000]; @@ -197,7 +199,9 @@ public override ZXSpectrum.CDLResult ReadCDL(ushort addr) { // ROM 0x000 case 0: - if (ROMPaged == 0) + if (TRDOSPaged) + result.Type = ZXSpectrum.CDLType.ROM2; + else if (ROMPaged == 0) result.Type = ZXSpectrum.CDLType.ROM0; else result.Type = ZXSpectrum.CDLType.ROM1; @@ -260,48 +264,21 @@ public override void WriteMemory(ushort addr, byte value) } /// - /// Checks whether supplied address is in a potentially contended bank + /// Checks whether supplied address is in a potentially contended bank. + /// The Pentagon is built from discrete TTL logic that interleaves CPU and video RAM access, + /// so the CPU clock is never stolen: there is NO memory contention on any bank. Always false. /// public override bool IsContended(ushort addr) { - var a = addr & 0xc000; - - if (a == 0x4000) - { - // low port contention - return true; - } - - if (a == 0xc000) - { - // high port contention - check for contended bank paged in - switch (RAMPaged) - { - case 1: - case 3: - case 5: - case 7: - return true; - } - } - return false; } /// - /// Returns TRUE if there is a contended bank paged in + /// Returns TRUE if there is a contended bank paged in. + /// The Pentagon has no contended banks (no contention at all), so always false. /// public override bool ContendedBankPaged() { - switch (RAMPaged) - { - case 1: - case 3: - case 5: - case 7: - return true; - } - return false; } @@ -330,19 +307,42 @@ public override byte FetchScreenMemory(ushort addr) return value; } + /// + /// Drives the Beta 128 automatic TR-DOS ROM switch from the Z80 M1 fetch address. + /// Page TR-DOS in when an opcode is fetched from 0x3D00-0x3DFF while the 48K BASIC ROM (ROM1) is + /// selected; page it back out on the first opcode fetched from outside the low 16K ROM window + /// (address 0x4000 and above). This runs before the opcode byte is read, so the instruction at + /// 0x3Dxx is fetched from the TR-DOS ROM, which is where its entry code lives. + /// + public override void TrapTrDos(ushort addr) + { + if (!TRDOSPaged) + { + if (ROMPaged == 1 && addr >= 0x3D00 && addr <= 0x3DFF) + TRDOSPaged = true; + } + else if (addr >= 0x4000) + { + TRDOSPaged = false; + } + } + /// /// Sets up the ROM /// public override void InitROM(RomData romData) { RomData = romData; - // 128k uses ROM0 and ROM1 - // 128k loader is in ROM0, and fallback 48k rom is in ROM1 + // The Pentagon EPROM image is three 16K banks concatenated: ROM0 = 128K editor/menu, + // ROM1 = 48K BASIC, ROM2 = TR-DOS. ROM2 is overlaid into the low 16K by the Beta 128 + // automatic switch (see TrapTrDos). for (int i = 0; i < 0x4000; i++) { ROM0[i] = RomData.RomBytes[i]; if (RomData.RomBytes.Length > 0x4000) ROM1[i] = RomData.RomBytes[i + 0x4000]; + if (RomData.RomBytes.Length > 0x8000) + ROM2[i] = RomData.RomBytes[i + 0x8000]; } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Port.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Port.cs index b9775db98db..ec01d1cc57f 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Port.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Port.cs @@ -16,24 +16,15 @@ public override byte ReadPort(ushort port) int result = 0xFF; - // ports 0x3ffd & 0x7ffd - // traditionally thought to be write-only - if (port == 0x3ffd || port == 0x7ffd) - { - // https://faqwiki.zxnet.co.uk/wiki/ZX_Spectrum_128 - // HAL bugs - // Reads from port 0x7ffd cause a crash, as the 128's HAL10H8 chip does not distinguish between reads and writes to this port, - // resulting in a floating data bus being used to set the paging registers. - - // -asni (2018-06-08) - need this to pass the final portread tests from fusetest.tap - - // get the floating bus value - ULADevice.ReadFloatingBus((int)CurrentFrameCycle, ref result, port); - // use this to set the paging registers - WritePort(port, (byte)result); - // return the floating bus value + // Beta 128 disk ports (0x1F/0x3F/0x5F/0x7F/0xFF) are only decoded while TR-DOS is paged in, and + // take priority over the Kempston/keyboard decode of 0x1F. + if (TRDOSPaged && UPDDiskDevice != null && UPDDiskDevice.ReadPort(port, ref result)) return (byte)result; - } + + // NOTE: the 128K/+2 has a HAL10H8 bug where reading 0x7ffd feeds the floating bus into the + // paging latch. That is Sinclair-specific silicon and does NOT apply to the Pentagon's discrete + // logic, so there is no special-case here: a read of 0x7ffd/0x3ffd falls through to the normal + // even-address keyboard/ULA handling and never disturbs the paging registers. // check AY if (AYDevice.ReadPort(port, ref result)) @@ -77,6 +68,10 @@ public override byte ReadPort(ushort port) /// public override void WritePort(ushort port, byte value) { + // Beta 128 disk ports, only while TR-DOS is paged in (see ReadPort). + if (TRDOSPaged && UPDDiskDevice != null && UPDDiskDevice.WritePort(port, value)) + return; + // get a BitArray of the port BitArray portBits = new BitArray(BitConverter.GetBytes(port)); // get a BitArray of the value byte @@ -118,6 +113,11 @@ public override void WritePort(ushort port, byte value) // Bit 5 set signifies that paging is disabled until next reboot PagingDisabled = bits[5]; + + // paging changed → rebuild PageContended (and the memory map) so the per-cycle + // contention decode isn't stale. Pentagon uses the fallback memory path, but + // PageContended is consulted every memory cycle, so this MUST run on paging changes. + RebuildMemoryMap(); } else { @@ -152,6 +152,7 @@ Bit 7 6 5 4 3 2 1 0 } // Buzzer + BuzzerDevice.SetClock((int)CurrentFrameCycle); BuzzerDevice.ProcessPulseValue((value & EAR_BIT) != 0, _renderSound); TapeDevice.WritePort(port, value); diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Screen.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Screen.cs index 31e01296d1d..ce0e38d2764 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Screen.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.Screen.cs @@ -9,34 +9,57 @@ internal class ScreenPentagon128 : ULA public ScreenPentagon128(SpectrumBase machine) : base(machine) { - // interrupt - InterruptStartTime = 0;// 3; - InterruptLength = 32; - // offsets - RenderTableOffset = 58; + // interrupt (Fuse: the /INT pulse is held for 36 T-states on the Pentagon) + InterruptStartTime = 0; + InterruptLength = 36; + // RenderTableOffset slides the border relative to the paper by 2px (1 T-state) per unit. There is a + // genuine 2px quantization we cannot fully resolve with this alone (the true fix is the Z80 OUT + // sub-instruction write instant - see the Pentagon plan doc): offset 0 leaves full-screen + // border+paper checkerboards 2px out of phase but keeps sparse-border scenes clean; offset 1 aligns + // the checkerboards but leaves a 1-T red sliver at the paper/right-border seam of full-overscan + // effects on alternate lines. We ship offset 1 (aligned checkerboard) as the more commonly-visible + // case. Fuse anchors the paper at top_left_pixel 17988 = FirstPaperLine 80 * 224 + FirstPaperTState + // 68 (already matched). The old 58 was the Sinclair ULA's pipeline fudge (pushed border ~57 T late, + // tearing full-screen effects) and does not belong here. ContentionOffset/FloatingBusOffset unused. + RenderTableOffset = 1; ContentionOffset = 6; FloatingBusOffset = 1; - // timing - ClockSpeed = 3546900; + // timing: Pentagon Z80 runs at 3.584 MHz (3584000 / 71680 T = exactly 50 Hz), per Fuse's + // libspectrum timings. (The old 3546900 was the Sinclair 128K clock, copied by mistake.) + ClockSpeed = 3584000; FrameCycleLength = 71680; ScanlineTime = 224; + // symmetric visible border (paper centred). The Pentagon's true overscan is asymmetric (Fuse: + // left 36T/right 28T, top 64/bottom 48 lines), but showing it raw leaves the paper off-centre and + // gives an odd buffer aspect; a symmetric 48px/48-line border keeps the image centred and cleanly + // proportioned. Border timing accuracy (the tear fix) is independent of these display extents. BorderLeftTime = 24; BorderRightTime = 24; FirstPaperLine = 80; FirstPaperTState = 68; - // screen layout + // screen layout. + // Border4T stays FALSE so the border resolves at 2px (one colour change per T-state), matching the + // fine diagonal border curves the real machine produces in full-screen border demos. Fuse snaps the + // border to a 4-T-state/8px column grid (its border_change queue stores a beam column, not a tstate), + // which hides sub-8px seams but visibly coarsens those diagonal curves - so we do NOT copy that; the + // hardware (and the reference capture) show 2px border resolution. Border4T = false; Border4TStage = 1; ScreenWidth = 256; ScreenHeight = 192; - BorderTopHeight = 48; // 55; // 48; - BorderBottomHeight = 48; // 56; + BorderTopHeight = 48; + BorderBottomHeight = 48; BorderLeftWidth = 48; BorderRightWidth = 48; ScanLineWidth = BorderLeftWidth + ScreenWidth + BorderRightWidth; + // Build the render table as Pentagon128 (not ZXSpectrum128). The machine type only affects the + // contention pattern and the floating-bus behaviour, not the render timing (which comes from the + // ULA fields set above). With no Pentagon case, CreateContention falls through to an all-zero + // pattern (correct: the Pentagon has no contention) and ReadFloatingBus leaves the caller's value + // untouched (correct: an unmapped port reads 0xFF, there is no video floating bus). RenderingTable = new RenderTable(this, - MachineType.ZXSpectrum128); + MachineType.Pentagon128); SetupScreenSize(); } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.cs index a6896cc1db3..41e9b633d55 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/Pentagon128K/Pentagon128.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using BizHawk.Emulation.Cores.Sound; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum @@ -12,7 +12,7 @@ public partial class Pentagon128 : SpectrumBase /// /// Main constructor /// - public Pentagon128(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public Pentagon128(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) { Spectrum = spectrum; CPU = cpu; @@ -40,6 +40,16 @@ public Pentagon128(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum TapeDevice = new DatacorderDevice(spectrum.SyncSettings.AutoLoadTape); TapeDevice.Init(this); + // Beta 128 disk interface (WD1793 + TR-DOS). Set up before media load so a .TRD can be inserted. + var beta = new BetaDiskController + { + DriveType = spectrum.SyncSettings.PentagonDriveType == ZXSpectrum.PentagonDiskDriveType.Drive525Inch + ? BetaDiskController.DriveKind.FiveQuarterInch + : BetaDiskController.DriveKind.ThreeHalfInch, + }; + UPDDiskDevice = beta; + UPDDiskDevice.Init(this); + InitializeMedia(files); } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs index 7b1269434c5..3131f85362d 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Text; using BizHawk.Common.StringExtensions; +using BizHawk.Emulation.Cores.Floppy; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { @@ -26,6 +27,12 @@ public abstract partial class SpectrumBase /// public List diskImages { get; set; } + /// + /// The side to present for each entry in diskImages: 0 or 1 selects one side of a + /// double-sided image (which is registered as two disks), -1 loads the image as-is. + /// + public List diskSides { get; set; } + /// /// Set when a savestate is loaded /// (Used to cancel any tape/disk load messages after a loadstate) @@ -64,11 +71,12 @@ public int TapeMediaIndex // load the media into the tape device tapeMediaIndex = result; + // load first so the tape blocks (and detected loader) are current, then fire the osd message + LoadTapeMedia(); + // fire osd message if (!IsLoadState) Spectrum.OSD_TapeInserted(); - - LoadTapeMedia(); } } @@ -104,11 +112,11 @@ public int DiskMediaIndex // load the media into the disk device diskMediaIndex = result; - // fire osd message + LoadDiskMedia(); + + // fire osd message (after load, so it can report the detected protection) if (!IsLoadState) Spectrum.OSD_DiskInserted(); - - LoadDiskMedia(); } } @@ -128,6 +136,7 @@ protected void LoadAllMedia() { tapeImages = new List(); diskImages = new List(); + diskSides = new List(); int cnt = 0; foreach (var m in mediaImages) @@ -139,64 +148,8 @@ protected void LoadAllMedia() Spectrum._tapeInfo.Add(Spectrum._gameInfo[cnt]); break; case SpectrumMediaType.Disk: - diskImages.Add(m); - Spectrum._diskInfo.Add(Spectrum._gameInfo[cnt]); - break; case SpectrumMediaType.DiskDoubleSided: - // this is a bit tricky. we will attempt to parse the double sided disk image byte array, - // then output two separate image byte arrays - List working = new List(); - foreach (DiskType type in Enum.GetValues(typeof(DiskType))) - { - bool found = false; - - switch (type) - { - case DiskType.CPCExtended: - found = CPCExtendedFloppyDisk.SplitDoubleSided(m, working); - break; - case DiskType.CPC: - found = CPCFloppyDisk.SplitDoubleSided(m, working); - break; - case DiskType.UDI: - found = UDI1_0FloppyDisk.SplitDoubleSided(m, working); - break; - } - - if (found) - { - // add side 1 - diskImages.Add(working[0]); - // add side 2 - diskImages.Add(working[1]); - - Common.GameInfo one = new Common.GameInfo(); - Common.GameInfo two = new Common.GameInfo(); - var gi = Spectrum._gameInfo[cnt]; - for (int i = 0; i < 2; i++) - { - Common.GameInfo work = new Common.GameInfo(); - if (i == 0) - { - work = one; - } - else if (i == 1) - { - work = two; - } - - work.FirmwareHash = gi.FirmwareHash; - work.Hash = gi.Hash; - work.Name = gi.Name + " (Parsed Side " + (i + 1) + ")"; - work.Region = gi.Region; - work.NotInDatabase = gi.NotInDatabase; - work.Status = gi.Status; - work.System = gi.System; - - Spectrum._diskInfo.Add(work); - } - } - } + AddDiskImage(m, Spectrum._gameInfo[cnt]); break; } @@ -210,6 +163,45 @@ protected void LoadAllMedia() LoadDiskMedia(); } + /// + /// Registers a disk image, splitting a double-sided disk into two selectable single-sided disks. This + /// works for every supported format (DSK/EDSK/IPF/HFE/SCP/FDI/UDI) because the double-sidedness is + /// determined from the shared flux model rather than per-format byte layouts. + /// + private void AddDiskImage(byte[] image, Common.GameInfo gameInfo) + { + int sides; + try { sides = DiskImageLoader.ToFluxDisk(image).Sides; } + catch { sides = 1; } + + // The +3's 3" drive is single-headed, so a double-sided image is split into two selectable disks. + // The Beta 128 drive (Pentagon) is double-headed and reads both sides of one disk, so never split. + if (sides <= 1 || this is Pentagon128) + { + diskImages.Add(image); + diskSides.Add(-1); + Spectrum._diskInfo.Add(gameInfo); + return; + } + + // double-sided: register both sides as separate disks (the +3 drive is single-headed) + for (int s = 0; s < 2; s++) + { + diskImages.Add(image); + diskSides.Add(s); + Spectrum._diskInfo.Add(new Common.GameInfo + { + FirmwareHash = gameInfo.FirmwareHash, + Hash = gameInfo.Hash, + Name = gameInfo.Name + " (Side " + (s + 1) + ")", + Region = gameInfo.Region, + NotInDatabase = gameInfo.NotInDatabase, + Status = gameInfo.Status, + System = gameInfo.System, + }); + } + } + /// /// Attempts to load a tape into the tape device based on tapeMediaIndex /// @@ -223,13 +215,26 @@ protected void LoadTapeMedia() /// protected void LoadDiskMedia() { - if (this is not ZX128Plus3) + var image = diskImages[diskMediaIndex]; + + // route by format to the machine that can actually read it, and warn (like the +3 gate) if the + // wrong model is selected: TR-DOS .trd/.scl need the Pentagon's Beta 128, every other disk image + // (+3/PCW .dsk/.edsk and the flux formats) needs the +3's uPD765. + bool isTrDos = Floppy.TrdConverter.IsTrd(image) || Floppy.SclConverter.IsScl(image); + + if (isTrDos && this is not Pentagon128) + { + Spectrum.CoreComm.ShowMessage("You are trying to load a TR-DOS (.trd/.scl) disk image.\n\n Please select Pentagon 128 emulation immediately and reboot the core"); + return; + } + + if (!isTrDos && this is not ZX128Plus3) { - Spectrum.CoreComm.ShowMessage("You are trying to load one of more disk images.\n\n Please select ZX Spectrum +3 emulation immediately and reboot the core"); + Spectrum.CoreComm.ShowMessage("You are trying to load a +3 disk image.\n\n Please select ZX Spectrum +3 emulation immediately and reboot the core"); return; } - UPDDiskDevice.FDD_LoadDisk(diskImages[diskMediaIndex]); + UPDDiskDevice.FDD_LoadDisk(image, diskSides[diskMediaIndex]); } /// @@ -277,6 +282,21 @@ private SpectrumMediaType IdentifyMedia(byte[] data) } } + // SuperCard Pro (.scp) and HxC (.hfe) flux images. These carry sidedness inside the flux rather + // than in a fixed header field, so AddDiskImage derives the side count from the decoded flux; + // classifying as Disk here is enough to route them down the disk path. + if (Floppy.ScpConverter.IsScp(data) + || Floppy.HfeConverter.IsHfe(data) || Floppy.HfeConverter.IsHfeV3(data)) + { + return SpectrumMediaType.Disk; + } + + // TR-DOS disk images: .trd is headerless (validated structurally), .scl carries a SINCLAIR header + if (Floppy.TrdConverter.IsTrd(data) || hdr.StartsWithIgnoreCase("SINCLAIR")) + { + return SpectrumMediaType.Disk; + } + // tape checking if (hdr.StartsWithIgnoreCase("ZXTAPE!")) { diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs index 4988352ff8d..fe41d9c3410 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs @@ -32,6 +32,13 @@ public abstract partial class SpectrumBase /// public bool SHADOWPaged; + /// + /// Signs that the TR-DOS ROM is currently paged into the low 16K. + /// Pentagon only: the Beta 128 interface overlays its TR-DOS ROM over the low 16K when the CPU + /// executes an opcode in 0x3D00-0x3DFF, and removes it when execution leaves 0x0000-0x3FFF. + /// + public bool TRDOSPaged; + /// /// Index of the current RAM page /// /// 128k, +2/2a and +3 only @@ -55,6 +62,15 @@ public virtual int _ROMpaged set => ROMPaged = value; } + /// + /// Called on every Z80 M1 opcode fetch (before the opcode byte is read), with the address being + /// fetched. The base machine does nothing; the Pentagon overrides this to page the TR-DOS ROM in + /// and out based on the fetch address (the Beta 128 automatic EPROM switch). + /// + public virtual void TrapTrDos(ushort addr) + { + } + /* * +3/+2A only */ @@ -84,6 +100,97 @@ public virtual int _ROMpaged /// public byte LastContendedReadByte; + // ---- Devirtualised memory access (opt-in per model) ---- + // When a model builds these maps, ReadMemoryMapped/WriteMemoryMapped index them directly — a + // non-virtual, inlinable path — instead of the virtual ReadMemory/WriteMemory (which the net48 + // JIT cannot devirtualise through a SpectrumBase reference). Indexed by 16K region (addr >> 14); + // a null _writeMap entry marks a read-only (ROM) region. + // These are NOT serialized — they are derived from the (serialized) banks + paging state and + // rebuilt via RebuildMemoryMap() at init AND after savestate load (loading may reallocate the + // bank arrays the maps point at, which would otherwise leave the maps stale). + protected byte[][] _readMap; + protected byte[][] _writeMap; + + /// + /// Per-16K-page (addr >> 14) precomputed copy of IsContended, so the per-memory-cycle contention + /// decode in CPUMonitor reads a non-virtual array instead of calling the virtual IsContended + /// (+ its paging switch on 128K/+2a) every cycle. Allocated once and mutated IN PLACE by + /// RebuildPageContention() (so a cached reference stays valid) — NOT serialized, rebuilt from the + /// (serialized) paging state exactly like _readMap/_writeMap. + /// + public readonly bool[] PageContended = new bool[4]; + + /// + /// Rebuilds the memory maps from the current paging state. Base default: no map, so models that + /// don't override fall back to the virtual ReadMemory/WriteMemory (unchanged behaviour). + /// + public virtual void RebuildMemoryMap() + { + _readMap = null; + _writeMap = null; + RebuildPageContention(); + } + + /// + /// Recomputes PageContended[] from the current paging state. Universal across models: it evaluates + /// the (virtual) IsContended at each 16K page boundary, so it matches IsContended exactly for + /// every model. Called from every RebuildMemoryMap() path (base + each override), which the + /// existing plumbing already invokes on paging changes, resets, and savestate load. + /// + protected void RebuildPageContention() + { + PageContended[0] = IsContended(0x0000); + PageContended[1] = IsContended(0x4000); + PageContended[2] = IsContended(0x8000); + PageContended[3] = IsContended(0xC000); + } + + /// + /// Devirtualised memory read; falls back to the virtual ReadMemory when no map is built. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public byte ReadMemoryMapped(ushort addr) + { + var m = _readMap; + return m == null ? ReadMemory(addr) : m[addr >> 14][addr & 0x3FFF]; + } + + /// + /// Devirtualised memory write; writes to read-only (null) regions are ignored; falls back when + /// no map is built. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void WriteMemoryMapped(ushort addr, byte value) + { + if (EventDrivenDisplay) ScreenWriteCatchUp(addr); + var m = _writeMap; + if (m == null) { WriteMemory(addr, value); return; } + var bank = m[addr >> 14]; + if (bank != null) bank[addr & 0x3FFF] = value; + } + + /// + /// When true, the ULA display is rendered event-driven — a catch-up render up to the current frame + /// cycle before each screen/border change and at frame end — instead of per-T-state from + /// CycleClock. Default false = exactly the current per-cycle behaviour (the hooks below are dormant + /// and add only a predicted-not-taken bool test). This is both the revert switch and the A/B toggle + /// for the event-driven-display experiment; NOT serialized (transient rendering strategy). + /// + public bool EventDrivenDisplay; + + /// + /// Event-driven display hook: if addr is in the currently-displayed screen region, render up to the + /// current frame cycle BEFORE the write lands, so the write only affects not-yet-drawn cycles (this + /// is what preserves per-cycle display accuracy for beam-racing effects). Base impl covers the fixed + /// lower screen 0x4000-0x5AFF (48K/16K, and 128K's normal screen mapped at 0x4000). 128K shadow + /// screen (RAM7) and 0xC000-mapped screen writes need a model override — TODO before enabling 128K. + /// + protected virtual void ScreenWriteCatchUp(ushort addr) + { + if (_render && (uint)(addr - 0x4000) < 0x1B00) + ULADevice.RenderScreen((int)CurrentFrameCycle); + } + /// /// Simulates reading from the bus /// Paging should be handled here diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs index 6efeb25db1e..7e18c5a7098 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs @@ -1,5 +1,5 @@ using BizHawk.Common; -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using BizHawk.Emulation.Cores.Sound; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum @@ -18,7 +18,7 @@ public abstract partial class SpectrumBase /// /// Reference to the instantiated Z80 cpu (piped in via constructor) /// - public Z80A CPU { get; set; } + public Z80AOpt CPU { get; set; } /// /// ROM and extended info @@ -64,7 +64,7 @@ public abstract partial class SpectrumBase /// /// The +3 built-in disk drive /// - public virtual NECUPD765 UPDDiskDevice { get; set; } + public virtual IFloppyDiskController UPDDiskDevice { get; set; } /// /// Holds the currently selected joysticks @@ -104,7 +104,8 @@ public abstract partial class SpectrumBase /// /// Gets the current frame cycle according to the CPU tick count /// - public virtual long CurrentFrameCycle => CPU.TotalExecutedCycles - LastFrameStartCPUTick; + // non-virtual (no overrides) so this per-cycle-hot property can be inlined + public long CurrentFrameCycle => CPU.TotalExecutedCycles - LastFrameStartCPUTick; /// /// Non-Deterministic bools @@ -150,12 +151,15 @@ public virtual void ExecuteFrame(bool render, bool renderSound) // run the CPU Monitor cycle CPUMon.ExecuteCycle(); - // clock the beepers - TapeBuzzer.SetClock((int)CurrentFrameCycle); - BuzzerDevice.SetClock((int)CurrentFrameCycle); + // The beepers are clocked event-driven now: SetClock is called immediately before each + // ProcessPulseValue (in the port-0xFE write handlers and the datacorder) rather than + // every T-state. clockCounter is only consumed by ProcessPulseValue, so the blip output + // is equivalent while removing two per-cycle calls from this hot loop. - // cycle the tape device - if (UPDDiskDevice == null || !UPDDiskDevice.FDD_IsDiskLoaded) + // Only cycle the tape while it is actually playing — TapeCycle is a no-op otherwise, and + // CPU tape reads (GetEarBit) are delta-based and driven from the port-read path, so this + // changes nothing functionally. + if (TapeDevice.TapeIsPlaying && (UPDDiskDevice == null || !UPDDiskDevice.FDD_IsDiskLoaded)) TapeDevice.TapeCycle(); // has frame end been reached? @@ -181,18 +185,6 @@ public virtual void ExecuteFrame(bool render, bool renderSound) // is this a lag frame? Spectrum.IsLagFrame = !InputRead; - - // FDC debug - if (UPDDiskDevice != null && UPDDiskDevice.writeDebug) - { - // only write UPD log every second - if (FrameCount % 10 == 0) - { - System.IO.File.AppendAllLines(UPDDiskDevice.outputfile, UPDDiskDevice.dLog); - UPDDiskDevice.dLog = new System.Collections.Generic.List(); - //System.IO.File.WriteAllText(UPDDiskDevice.outputfile, UPDDiskDevice.outputString); - } - } } /// @@ -245,6 +237,9 @@ public virtual void HardReset() r[i] = 0x00; } } + + // reset zeroed the paging state (ROMPaged / RAMPaged) — rebuild the devirtualisation map + RebuildMemoryMap(); } /// @@ -296,6 +291,9 @@ public virtual void SoftReset() r[i] = 0x00; } } + + // reset zeroed the paging state (ROMPaged / RAMPaged) — rebuild the devirtualisation map + RebuildMemoryMap(); } public void SyncState(Serializer ser) @@ -321,6 +319,7 @@ public void SyncState(Serializer ser) ser.Sync(nameof(RAM6), ref RAM6, false); ser.Sync(nameof(RAM7), ref RAM7, false); ser.Sync(nameof(ROMPaged), ref ROMPaged); + ser.Sync(nameof(TRDOSPaged), ref TRDOSPaged); ser.Sync(nameof(SHADOWPaged), ref SHADOWPaged); ser.Sync(nameof(RAMPaged), ref RAMPaged); ser.Sync(nameof(PagingDisabled), ref PagingDisabled); @@ -362,6 +361,11 @@ public void SyncState(Serializer ser) UPDDiskDevice?.SyncState(ser); + // Loading may have reallocated the bank arrays; rebuild the devirtualisation memory maps + // from the freshly-loaded paging state so they don't point at stale arrays. + if (ser.IsReader) + RebuildMemoryMap(); + ser.EndSection(); } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULA.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULA.cs index 0441fe297fe..445af407ae2 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULA.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULA.cs @@ -119,10 +119,12 @@ protected ULA(SpectrumBase machine) /// Cycles the ULA clock /// Handles interrupt generation /// - public virtual void CycleClock(long totalCycles) + // non-virtual (no model overrides it) so this per-T-state call is a direct dispatch, not a vtable lookup + public void CycleClock(long totalCycles) { - // render the screen - if (_machine._render) + // render the screen (skipped in event-driven mode, where catch-up renders fire on + // screen/border writes and at frame end instead) + if (_machine._render && !_machine.EventDrivenDisplay) RenderScreen((int)_machine.CurrentFrameCycle); // has more than one cycle past since this last ran @@ -162,7 +164,12 @@ public virtual void CycleClock(long totalCycles) _machine.CPU.FlagI = true; FrameEnd = true; ULACycleCounter = InterruptStartTime; - CalcFlashCounter(); + // event-driven: render this frame's remaining cycles up to now BEFORE flashOn + // flips below — the same CurrentFrameCycle and flash state that per-cycle + // mode used for its final render of the frame. + if (_machine.EventDrivenDisplay && _machine._render) + RenderScreen((int)_machine.CurrentFrameCycle); + CalcFlashCounter(); } } } @@ -261,6 +268,14 @@ public class RenderTable /// public RenderCycle[] Renderer; + /// + /// Flat per-frame-T-state copy of Renderer[t].ContentionValue — a contiguous int[] instead of + /// the pointer-chase into the RenderCycle class array, for the per-cycle contention hot path. + /// Built once here (Renderer is a pure function of static per-model timing), never mutated, + /// never serialized. Value-identical to Renderer[t].ContentionValue. + /// + public int[] ContentionByCycle; + /// /// The emulated machine /// @@ -277,6 +292,10 @@ public RenderTable(ULA ula, MachineType machineType) _machineType = machineType; Renderer = new RenderCycle[_ula.FrameCycleLength]; InitRenderer(machineType); + + ContentionByCycle = new int[_ula.FrameCycleLength]; + for (int t = 0; t < ContentionByCycle.Length; t++) + ContentionByCycle[t] = Renderer[t].ContentionValue; } /// @@ -735,7 +754,7 @@ public int GetContentionValue(int tstate) if (tstate < 0) tstate += FrameCycleLength; - return RenderingTable.Renderer[tstate].ContentionValue; + return RenderingTable.ContentionByCycle[tstate]; } /// @@ -749,7 +768,7 @@ public int GetPortContentionValue(int tstate) if (tstate < 0) tstate += FrameCycleLength; - return RenderingTable.Renderer[tstate].ContentionValue; + return RenderingTable.ContentionByCycle[tstate]; } /// @@ -807,7 +826,57 @@ public int VsyncNumerator public int VsyncDenominator => ClockSpeed; // FrameLength; + /// + /// When true, each displayed frame is averaged with the previous one. This mimics the phosphor + /// persistence of a real CRT/TV, which is what makes gigascreen effects (software that alternates two + /// images every frame for extra colours or resolution) resolve into their intended blended picture + /// instead of a harsh 25Hz flicker. Display-only: it never touches the emulated ScreenBuffer, the + /// emulation timing or savestates, so it is safe to toggle at runtime and does not desync movies. + /// + public bool FrameBlend = true; + + private int[] _blendBuffer; + private int[] _prevFrame; + private int[] _holdFrame; + private int _lastBlendFrame = -1; + public int[] GetVideoBuffer() + { + var cur = GetVideoBufferRaw(); + if (!FrameBlend) + return cur; + + if (_prevFrame == null || _prevFrame.Length != cur.Length) + { + _prevFrame = (int[])cur.Clone(); + _holdFrame = (int[])cur.Clone(); + _blendBuffer = new int[cur.Length]; + _lastBlendFrame = _machine?.FrameCount ?? 0; + } + + // Promote last frame's buffer to "previous" only when the emulated frame actually advances, so + // that several GetVideoBuffer calls within one frame (display + screenshot + A/V dump) all blend + // against the same prior frame rather than collapsing the blend. + int frame = _machine?.FrameCount ?? 0; + if (frame != _lastBlendFrame) + { + System.Array.Copy(_holdFrame, _prevFrame, cur.Length); + _lastBlendFrame = frame; + } + System.Array.Copy(cur, _holdFrame, cur.Length); + + for (int i = 0; i < cur.Length; i++) + { + int c = cur[i], p = _prevFrame[i]; + int r = ((((c >> 16) & 0xFF) + ((p >> 16) & 0xFF)) >> 1) & 0xFF; + int g = ((((c >> 8) & 0xFF) + ((p >> 8) & 0xFF)) >> 1) & 0xFF; + int b = (((c & 0xFF) + (p & 0xFF)) >> 1) & 0xFF; + _blendBuffer[i] = (0xFF << 24) | (r << 16) | (g << 8) | b; + } + return _blendBuffer; + } + + private int[] GetVideoBufferRaw() { switch (borderType) { diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs index af850364982..348e4e38170 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs @@ -330,6 +330,26 @@ public override byte FetchScreenMemory(ushort addr) return value; } + /// + /// Builds the devirtualised memory maps from the current paging state (ROMPaged / RAMPaged). + /// Mirrors ReadBus/WriteBus exactly: region 0 = ROM0/ROM1 (read-only), 1 = RAM5, 2 = RAM2, + /// 3 = the paged RAM bank. MUST be called on every paging change (0x7ffd write) and reset. + /// (SHADOWPaged only affects the ULA screen fetch, not the CPU map, so it is not consulted.) + /// + public override void RebuildMemoryMap() + { + byte[] rom = ROMPaged == 0 ? ROM0 : ROM1; + byte[] hi = RAMPaged switch + { + 0 => RAM0, 1 => RAM1, 2 => RAM2, 3 => RAM3, + 4 => RAM4, 5 => RAM5, 6 => RAM6, 7 => RAM7, + _ => RAM0, + }; + _readMap = new[] { rom, RAM5, RAM2, hi }; + _writeMap = new byte[][] { null, RAM5, RAM2, hi }; // ROM region is read-only + RebuildPageContention(); + } + /// /// Sets up the ROM /// @@ -344,6 +364,7 @@ public override void InitROM(RomData romData) if (RomData.RomBytes.Length > 0x4000) ROM1[i] = RomData.RomBytes[i + 0x4000]; } + RebuildMemoryMap(); } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs index 79d714fbfd6..b29040aae98 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs @@ -117,6 +117,9 @@ public override void WritePort(ushort port, byte value) // Bit 5 set signifies that paging is disabled until next reboot PagingDisabled = bits[5]; + + // paging (ROMPaged / RAMPaged) may have changed — rebuild the devirtualisation map + RebuildMemoryMap(); } else { @@ -151,6 +154,7 @@ Bit 7 6 5 4 3 2 1 0 } // Buzzer + BuzzerDevice.SetClock((int)CurrentFrameCycle); BuzzerDevice.ProcessPulseValue((value & EAR_BIT) != 0, _renderSound); TapeDevice.WritePort(port, value); diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs index 215ee8df71d..13af80ec81d 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using BizHawk.Emulation.Cores.Sound; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum @@ -12,7 +12,7 @@ public partial class ZX128 : SpectrumBase /// /// Main constructor /// - public ZX128(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public ZX128(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) { Spectrum = spectrum; CPU = cpu; diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs index 8f186714e02..11d99b9ffa2 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs @@ -1,4 +1,4 @@ -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum @@ -12,7 +12,7 @@ public sealed class ZX128Plus2 : ZX128 /// /// Main constructor /// - public ZX128Plus2(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public ZX128Plus2(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) : base(spectrum, cpu, borderType, files, joysticks) {} } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs index 3f43ec18f2c..3fd555b5e4e 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs @@ -87,6 +87,11 @@ public override void WritePort(ushort port, byte value) // portbit 4 is the LOW BIT of the ROM selection ROMlow = bits[4]; + + // paging changed → rebuild PageContended (and the memory map) so the per-cycle + // contention decode isn't stale. +2a uses the fallback memory path, but PageContended + // is still consulted every memory cycle, so this MUST run on every paging change. + RebuildMemoryMap(); } } // port 0x1ffd - hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set @@ -122,6 +127,9 @@ public override void WritePort(ushort port, byte value) // set the special paging mode flag SpecialPagingMode = true; } + + // paging changed → rebuild PageContended (see the 0x7ffd handler above) + RebuildMemoryMap(); } // bit 4 is the printer port strobe @@ -151,6 +159,7 @@ Bit 7 6 5 4 3 2 1 0 } // Buzzer + BuzzerDevice.SetClock((int)CurrentFrameCycle); BuzzerDevice.ProcessPulseValue((value & EAR_BIT) != 0, _renderSound); // Tape diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs index 41c1ab5b425..dffa28cd1f4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs @@ -1,4 +1,4 @@ -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; using BizHawk.Emulation.Cores.Sound; @@ -12,7 +12,7 @@ public partial class ZX128Plus2a : SpectrumBase /// /// Main constructor /// - public ZX128Plus2a(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public ZX128Plus2a(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) { Spectrum = spectrum; CPU = cpu; diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs index 1c64bb538a4..7a93df474f4 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs @@ -98,6 +98,11 @@ public override void WritePort(ushort port, byte value) // portbit 4 is the LOW BIT of the ROM selection ROMlow = bits[4]; + + // paging changed → rebuild PageContended (and the memory map) so the per-cycle + // contention decode isn't stale. +3 uses the fallback memory path, but PageContended + // is still consulted every memory cycle, so this MUST run on every paging change. + RebuildMemoryMap(); } } // port 0x1ffd - hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set @@ -131,6 +136,9 @@ public override void WritePort(ushort port, byte value) // set the special paging mode flag SpecialPagingMode = true; } + + // paging changed → rebuild PageContended (see the 0x7ffd handler above) + RebuildMemoryMap(); } // bit 4 is the printer port strobe @@ -158,6 +166,7 @@ Bit 7 6 5 4 3 2 1 0 } // Buzzer + BuzzerDevice.SetClock((int)CurrentFrameCycle); BuzzerDevice.ProcessPulseValue((value & EAR_BIT) != 0, _renderSound); // Tape diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs index 021f65c7261..ec4c4197b6b 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs @@ -1,4 +1,4 @@ -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; using BizHawk.Emulation.Cores.Sound; @@ -12,7 +12,7 @@ public partial class ZX128Plus3 : SpectrumBase /// /// Main constructor /// - public ZX128Plus3(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public ZX128Plus3(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) { Spectrum = spectrum; CPU = cpu; @@ -40,7 +40,7 @@ public ZX128Plus3(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum. TapeDevice = new DatacorderDevice(spectrum.SyncSettings.AutoLoadTape); TapeDevice.Init(this); - UPDDiskDevice = new NECUPD765(); + UPDDiskDevice = new Upd765DiskController(); UPDDiskDevice.Init(this); InitializeMedia(files); diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs index 61b3ae4ece9..4c1fe4c1a7f 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs @@ -1,4 +1,4 @@ -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum @@ -11,7 +11,7 @@ public class ZX16 : ZX48 /// /// Main constructor /// - public ZX16(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public ZX16(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) : base(spectrum, cpu, borderType, files, joysticks) {} /* 48K Spectrum has NO memory paging @@ -114,6 +114,17 @@ public override void WriteMemory(ushort addr, byte value) WriteBus(addr, value); } + /// + /// 16K's layout differs from 48K (regions 2/3 are unmapped) so it does NOT use the 48K map — + /// stay on the virtual ReadMemory/WriteMemory path. (Overrides ZX48's map build.) + /// + public override void RebuildMemoryMap() + { + _readMap = null; + _writeMap = null; + RebuildPageContention(); + } + /// /// Sets up the ROM /// @@ -122,6 +133,7 @@ public override void InitROM(RomData romData) RomData = romData; // for 16/48k machines only ROM0 is used (no paging) RomData.RomBytes?.CopyTo(ROM0, 0); + RebuildMemoryMap(); } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs index d437f67d783..ad07768d4d0 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs @@ -141,6 +141,17 @@ public override bool ContendedBankPaged() return false; } + /// + /// 48K has a fixed memory layout (no paging): ROM0 (read-only) at 0x0000, then RAM banks + /// 0/1/2. Build the devirtualised maps once — they never change for 48K. + /// + public override void RebuildMemoryMap() + { + _readMap = new[] { ROM0, RAM0, RAM1, RAM2 }; + _writeMap = new byte[][] { null, RAM0, RAM1, RAM2 }; // ROM region is read-only + RebuildPageContention(); + } + /// /// Sets up the ROM /// @@ -149,6 +160,7 @@ public override void InitROM(RomData romData) RomData = romData; // for 16/48k machines only ROM0 is used (no paging) RomData.RomBytes?.CopyTo(ROM0, 0); + RebuildMemoryMap(); } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs index ee93dc27288..72977a11ac1 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs @@ -80,11 +80,14 @@ Bit 7 6 5 4 3 2 1 0 // Border - LSB 3 bits hold the border colour if (ULADevice.BorderColor != (value & BORDER_BIT)) { - //ULADevice.RenderScreen((int)CurrentFrameCycle); + // event-driven mode: draw up to now with the OLD border colour before it changes + if (EventDrivenDisplay && _render) + ULADevice.RenderScreen((int)CurrentFrameCycle); ULADevice.BorderColor = value & BORDER_BIT; } // Buzzer + BuzzerDevice.SetClock((int)CurrentFrameCycle); BuzzerDevice.ProcessPulseValue((value & EAR_BIT) != 0, _renderSound); // Tape diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs index 06803ed7e28..d63a2a21c32 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs @@ -1,4 +1,4 @@ -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; using BizHawk.Emulation.Cores.Sound; @@ -13,7 +13,7 @@ public partial class ZX48 : SpectrumBase /// /// Main constructor /// - public ZX48(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + public ZX48(ZXSpectrum spectrum, Z80AOpt cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) { Spectrum = spectrum; CPU = cpu; diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/CPCFormat/CPCExtendedFloppyDisk.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/CPCFormat/CPCExtendedFloppyDisk.cs deleted file mode 100644 index 1b89ad57f4b..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/CPCFormat/CPCExtendedFloppyDisk.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System.Text; -using BizHawk.Common; - -using System.Collections.Generic; -using BizHawk.Common.StringExtensions; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Logical object representing a standard +3 disk image - /// - public class CPCExtendedFloppyDisk : FloppyDisk - { - /// - /// The format type - /// - public override DiskType DiskFormatType => DiskType.CPCExtended; - - /// - /// Attempts to parse incoming disk data - /// - /// - /// TRUE: disk parsed - /// FALSE: unable to parse disk - /// - public override bool ParseDisk(byte[] data) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 16); - - if (!ident.ContainsIgnoreCase("EXTENDED CPC DSK")) - { - // incorrect format - return false; - } - - // read the disk information block - DiskHeader.DiskIdent = ident; - DiskHeader.DiskCreatorString = Encoding.ASCII.GetString(data, 0x22, 14); - DiskHeader.NumberOfTracks = data[0x30]; - DiskHeader.NumberOfSides = data[0x31]; - DiskHeader.TrackSizes = new int[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; - DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; - DiskData = data; - int pos = 0x34; - - if (DiskHeader.NumberOfSides > 1) - { - StringBuilder sbm = new StringBuilder(); - sbm.AppendLine(); - sbm.AppendLine(); - sbm.AppendLine("The detected disk image contains multiple sides."); - sbm.AppendLine("This is NOT currently supported in ZXHawk."); - sbm.AppendLine("Please find an alternate image/dump where each side has been saved as a separate *.dsk image (and use the multi-disk bundler tool to load into Bizhawk)."); - throw new System.NotImplementedException(sbm.ToString()); - } - - for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) - { - DiskHeader.TrackSizes[i] = data[pos++] * 256; - } - - // move to first track information block - pos = 0x100; - - // parse each track - for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) - { - // check for unformatted track - if (DiskHeader.TrackSizes[i] == 0) - { - DiskTracks[i] = new() { Sectors = Array.Empty() }; - continue; - } - - int p = pos; - DiskTracks[i] = new Track(); - - // track info block - DiskTracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12); - p += 16; - DiskTracks[i].TrackNumber = data[p++]; - DiskTracks[i].SideNumber = data[p++]; - DiskTracks[i].DataRate = data[p++]; - DiskTracks[i].RecordingMode = data[p++]; - DiskTracks[i].SectorSize = data[p++]; - DiskTracks[i].NumberOfSectors = data[p++]; - DiskTracks[i].GAP3Length = data[p++]; - DiskTracks[i].FillerByte = data[p++]; - - int dpos = pos + 0x100; - - // sector info list - DiskTracks[i].Sectors = new Sector[DiskTracks[i].NumberOfSectors]; - for (int s = 0; s < DiskTracks[i].NumberOfSectors; s++) - { - DiskTracks[i].Sectors[s] = new Sector(); - - DiskTracks[i].Sectors[s].TrackNumber = data[p++]; - DiskTracks[i].Sectors[s].SideNumber = data[p++]; - DiskTracks[i].Sectors[s].SectorID = data[p++]; - DiskTracks[i].Sectors[s].SectorSize = data[p++]; - DiskTracks[i].Sectors[s].Status1 = data[p++]; - DiskTracks[i].Sectors[s].Status2 = data[p++]; - DiskTracks[i].Sectors[s].ActualDataByteLength = MediaConverter.GetWordValue(data, p); - p += 2; - - // sector data - begins at 0x100 offset from the start of the track info block (in this case dpos) - DiskTracks[i].Sectors[s].SectorData = new byte[DiskTracks[i].Sectors[s].ActualDataByteLength]; - - // copy the data - for (int b = 0; b < DiskTracks[i].Sectors[s].ActualDataByteLength; b++) - { - DiskTracks[i].Sectors[s].SectorData[b] = data[dpos + b]; - } - - // check for multiple weak/random sectors stored - if (DiskTracks[i].Sectors[s].SectorSize <= 7) - { - // sectorsize n=8 is equivilent to n=0 - FDC will use DTL for length - int specifiedSize = 0x80 << DiskTracks[i].Sectors[s].SectorSize; - - if (specifiedSize < DiskTracks[i].Sectors[s].ActualDataByteLength) - { - // more data stored than sectorsize defines - // check for multiple weak/random copies - if (DiskTracks[i].Sectors[s].ActualDataByteLength % specifiedSize != 0) - { - DiskTracks[i].Sectors[s].ContainsMultipleWeakSectors = true; - } - } - } - - // move dpos to the next sector data postion - dpos += DiskTracks[i].Sectors[s].ActualDataByteLength; - } - - // move to the next track info block - pos += DiskHeader.TrackSizes[i]; - } - - // run protection scheme detector - ParseProtection(); - - return true; - } - - /// - /// Takes a double-sided disk byte array and converts into 2 single-sided arrays - /// - public static bool SplitDoubleSided(byte[] data, List results) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 16); - if (!ident.ContainsIgnoreCase("EXTENDED CPC DSK")) - { - // incorrect format - return false; - } - - byte[] S0 = new byte[data.Length]; - byte[] S1 = new byte[data.Length]; - - // disk info block - Array.Copy(data, 0, S0, 0, 0x100); - Array.Copy(data, 0, S1, 0, 0x100); - // change side number - S0[0x31] = 1; - S1[0x31] = 1; - - // extended format can have different track sizes - int[] trkSizes = new int[data[0x30] * data[0x31]]; - - int pos = 0x34; - for (int i = 0; i < data[0x30] * data[0x31]; i++) - { - trkSizes[i] = data[pos] * 256; - // clear destination trk sizes (will be added later) - S0[pos] = 0; - S1[pos] = 0; - pos++; - } - - // start at track info blocks - int mPos = 0x100; - int s0Pos = 0x100; - int s0tCount = 0; - int s1tCount = 0; - int s1Pos = 0x100; - int tCount = 0; - - while (tCount < data[0x30] * data[0x31]) - { - // which side is this? - var side = data[mPos + 0x11]; - if (side == 0) - { - // side 1 - Array.Copy(data, mPos, S0, s0Pos, trkSizes[tCount]); - s0Pos += trkSizes[tCount]; - // trk size table - S0[0x34 + s0tCount++] = (byte)(trkSizes[tCount] / 256); - } - else if (side == 1) - { - // side 2 - Array.Copy(data, mPos, S1, s1Pos, trkSizes[tCount]); - s1Pos += trkSizes[tCount]; - // trk size table - S1[0x34 + s1tCount++] = (byte)(trkSizes[tCount] / 256); - } - - mPos += trkSizes[tCount++]; - } - - byte[] s0final = new byte[s0Pos]; - byte[] s1final = new byte[s1Pos]; - Array.Copy(S0, 0, s0final, 0, s0Pos); - Array.Copy(S1, 0, s1final, 0, s1Pos); - - results.Add(s0final); - results.Add(s1final); - - return true; - } - - /// - /// State serlialization - /// - public override void SyncState(Serializer ser) - { - ser.BeginSection("Plus3FloppyDisk"); - - ser.Sync(nameof(CylinderCount), ref CylinderCount); - ser.Sync(nameof(SideCount), ref SideCount); - ser.Sync(nameof(BytesPerTrack), ref BytesPerTrack); - ser.Sync(nameof(WriteProtected), ref WriteProtected); - ser.SyncEnum(nameof(Protection), ref Protection); - - ser.Sync(nameof(DirtyData), ref DirtyData); - if (DirtyData) - { - //TODO - } - - // sync deterministic track and sector counters - ser.Sync(nameof(_randomCounter), ref _randomCounter); - RandomCounter = _randomCounter; - - ser.EndSection(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/CPCFormat/CPCFloppyDisk.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/CPCFormat/CPCFloppyDisk.cs deleted file mode 100644 index e68fc05775b..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/CPCFormat/CPCFloppyDisk.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System.Text; -using BizHawk.Common; - -using System.Collections.Generic; -using BizHawk.Common.StringExtensions; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Logical object representing a standard +3 disk image - /// - public class CPCFloppyDisk : FloppyDisk - { - /// - /// The format type - /// - public override DiskType DiskFormatType => DiskType.CPC; - - /// - /// Attempts to parse incoming disk data - /// - /// - /// TRUE: disk parsed - /// FALSE: unable to parse disk - /// - public override bool ParseDisk(byte[] data) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 16); - - if (!ident.ContainsIgnoreCase("MV - CPC")) - { - // incorrect format - return false; - } - - // read the disk information block - DiskHeader.DiskIdent = ident; - DiskHeader.DiskCreatorString = Encoding.ASCII.GetString(data, 0x22, 14); - DiskHeader.NumberOfTracks = data[0x30]; - DiskHeader.NumberOfSides = data[0x31]; - DiskHeader.TrackSizes = new int[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; - DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; - DiskData = data; - int pos = 0x32; - - if (DiskHeader.NumberOfSides > 1) - { - StringBuilder sbm = new StringBuilder(); - sbm.AppendLine(); - sbm.AppendLine(); - sbm.AppendLine("The detected disk image contains multiple sides."); - sbm.AppendLine("This is NOT currently supported in ZXHawk."); - sbm.AppendLine("Please find an alternate image/dump where each side has been saved as a separate *.dsk image (and use the multi-disk bundler tool to load into Bizhawk)."); - throw new NotImplementedException(sbm.ToString()); - } - - // standard CPC format all track sizes are the same in the image - for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) - { - DiskHeader.TrackSizes[i] = MediaConverter.GetWordValue(data, pos); - } - - // move to first track information block - pos = 0x100; - - // parse each track - for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) - { - // check for unformatted track - if (DiskHeader.TrackSizes[i] == 0) - { - DiskTracks[i] = new() { Sectors = Array.Empty() }; - continue; - } - - int p = pos; - DiskTracks[i] = new Track(); - - // track info block - DiskTracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12); - p += 16; - DiskTracks[i].TrackNumber = data[p++]; - DiskTracks[i].SideNumber = data[p++]; - p += 2; - DiskTracks[i].SectorSize = data[p++]; - DiskTracks[i].NumberOfSectors = data[p++]; - DiskTracks[i].GAP3Length = data[p++]; - DiskTracks[i].FillerByte = data[p++]; - - int dpos = pos + 0x100; - - // sector info list - DiskTracks[i].Sectors = new Sector[DiskTracks[i].NumberOfSectors]; - for (int s = 0; s < DiskTracks[i].NumberOfSectors; s++) - { - DiskTracks[i].Sectors[s] = new Sector(); - - DiskTracks[i].Sectors[s].TrackNumber = data[p++]; - DiskTracks[i].Sectors[s].SideNumber = data[p++]; - DiskTracks[i].Sectors[s].SectorID = data[p++]; - DiskTracks[i].Sectors[s].SectorSize = data[p++]; - DiskTracks[i].Sectors[s].Status1 = data[p++]; - DiskTracks[i].Sectors[s].Status2 = data[p++]; - DiskTracks[i].Sectors[s].ActualDataByteLength = MediaConverter.GetWordValue(data, p); - p += 2; - - // actualdatabytelength value is calculated now - if (DiskTracks[i].Sectors[s].SectorSize == 0) - { - // no sectorsize specified - DTL will be used at runtime - DiskTracks[i].Sectors[s].ActualDataByteLength = DiskHeader.TrackSizes[i]; - } - else if (DiskTracks[i].Sectors[s].SectorSize > 6) - { - // invalid - wrap around to 0 - DiskTracks[i].Sectors[s].ActualDataByteLength = DiskHeader.TrackSizes[i]; - } - else if (DiskTracks[i].Sectors[s].SectorSize == 6) - { - // only 0x1800 bytes are stored - DiskTracks[i].Sectors[s].ActualDataByteLength = 0x1800; - } - else - { - // valid sector size for this format - DiskTracks[i].Sectors[s].ActualDataByteLength = 0x80 << DiskTracks[i].Sectors[s].SectorSize; - } - - // sector data - begins at 0x100 offset from the start of the track info block (in this case dpos) - DiskTracks[i].Sectors[s].SectorData = new byte[DiskTracks[i].Sectors[s].ActualDataByteLength]; - - // copy the data - for (int b = 0; b < DiskTracks[i].Sectors[s].ActualDataByteLength; b++) - { - DiskTracks[i].Sectors[s].SectorData[b] = data[dpos + b]; - } - - // move dpos to the next sector data postion - dpos += DiskTracks[i].Sectors[s].ActualDataByteLength; - } - - // move to the next track info block - pos += DiskHeader.TrackSizes[i]; - } - - // run protection scheme detector - ParseProtection(); - - return true; - } - - /// - /// Takes a double-sided disk byte array and converts into 2 single-sided arrays - /// - public static bool SplitDoubleSided(byte[] data, List results) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 16); - if (!ident.ContainsIgnoreCase("MV - CPC")) - { - // incorrect format - return false; - } - - byte[] S0 = new byte[data.Length]; - byte[] S1 = new byte[data.Length]; - - // disk info block - Array.Copy(data, 0, S0, 0, 0x100); - Array.Copy(data, 0, S1, 0, 0x100); - // change side number - S0[0x31] = 1; - S1[0x31] = 1; - - var trkSize = MediaConverter.GetWordValue(data, 0x32); - - // start at track info blocks - int mPos = 0x100; - int s0Pos = 0x100; - int s1Pos = 0x100; - - var numTrks = data[0x30]; - var numSides = data[0x31]; - - while (mPos < trkSize * data[0x30] * data[0x31]) - { - // which side is this? - var side = data[mPos + 0x11]; - if (side == 0) - { - // side 1 - Array.Copy(data, mPos, S0, s0Pos, trkSize); - s0Pos += trkSize; - } - else if (side == 1) - { - // side 2 - Array.Copy(data, mPos, S1, s1Pos, trkSize); - s1Pos += trkSize; - } - - mPos += trkSize; - } - - byte[] s0final = new byte[s0Pos]; - byte[] s1final = new byte[s1Pos]; - Array.Copy(S0, 0, s0final, 0, s0Pos); - Array.Copy(S1, 0, s1final, 0, s1Pos); - - results.Add(s0final); - results.Add(s1final); - - return true; - } - - /// - /// State serlialization - /// - public override void SyncState(Serializer ser) - { - ser.BeginSection("Plus3FloppyDisk"); - - ser.Sync(nameof(CylinderCount), ref CylinderCount); - ser.Sync(nameof(SideCount), ref SideCount); - ser.Sync(nameof(BytesPerTrack), ref BytesPerTrack); - ser.Sync(nameof(WriteProtected), ref WriteProtected); - ser.SyncEnum(nameof(Protection), ref Protection); - - ser.Sync(nameof(DirtyData), ref DirtyData); - if (DirtyData) - { - //TODO - } - - ser.EndSection(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/DiskType.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/DiskType.cs deleted file mode 100644 index d403ec6ffa7..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/DiskType.cs +++ /dev/null @@ -1,34 +0,0 @@ - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// The different disk formats ZXHawk currently supports - /// - public enum DiskType - { - /// - /// Standard CPCEMU disk format (used in the built-in +3 disk drive) - /// - CPC, - - /// - /// Extended CPCEMU disk format (used in the built-in +3 disk drive) - /// - CPCExtended, - - /// - /// Interchangeable Preservation Format - /// - IPF, - - /// - /// Ultra Disk Image Format (v1.0) - /// - UDI, - - /// - /// Ultra Disk Image Format (v1.1) - /// - UDIv1_1 - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/FloppyDisk.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/FloppyDisk.cs deleted file mode 100644 index cbe56f61e13..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/FloppyDisk.cs +++ /dev/null @@ -1,710 +0,0 @@ -using BizHawk.Common; - -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using BizHawk.Common.CollectionExtensions; -using BizHawk.Common.StringExtensions; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// This abstract class defines a logical floppy disk - /// - public abstract class FloppyDisk - { - /// - /// The disk format type - /// - public abstract DiskType DiskFormatType { get; } - - /// - /// Disk information header - /// - public Header DiskHeader = new Header(); - - /// - /// Track array - /// - public Track[] DiskTracks = null; - - /// - /// No. of tracks per side - /// - public int CylinderCount; - - /// - /// The number of physical sides - /// - public int SideCount; - - /// - /// The number of bytes per track - /// - public int BytesPerTrack; - - /// - /// The write-protect tab on the disk - /// - public bool WriteProtected; - - /// - /// The detected protection scheme (if any) - /// - public ProtectionType Protection; - - /// - /// The actual disk image data - /// - public byte[] DiskData; - - /// - /// If TRUE then data on the disk has changed (been written to) - /// This will be used to determine whether the disk data needs to be included - /// in any SyncState operations - /// - protected bool DirtyData = false; - - /// - /// Used to deterministically choose a 'random' sector when dealing with weak reads - /// - public int RandomCounter - { - get => _randomCounter; - set - { - _randomCounter = value; - - foreach (var trk in DiskTracks) - { - foreach (var sec in trk.Sectors) - { - sec.RandSecCounter = _randomCounter; - } - } - } - } - protected int _randomCounter; - - - /// - /// Attempts to parse incoming disk data - /// - /// - /// TRUE: disk parsed - /// FALSE: unable to parse disk - /// - public virtual bool ParseDisk(byte[] diskData) - { - // default result - // override in inheriting class - return false; - } - - /// - /// Examines the floppydisk data to work out what protection (if any) is present - /// If possible it will also fix the disk data for this protection - /// This should be run at the end of the ParseDisk() method - /// - public virtual void ParseProtection() - { - int[] weakArr = new int[2]; - - // speedlock - if (DetectSpeedlock(ref weakArr)) - { - Protection = ProtectionType.Speedlock; - - Sector sec = DiskTracks[0].Sectors[1]; - if (!sec.ContainsMultipleWeakSectors) - { - byte[] origData = sec.SectorData.ToArray(); - List data = new(); //TODO pretty sure the length and indices here are known in advance and this can just be an array --yoshi - for (int m = 0; m < 3; m++) - { - for (int i = 0; i < 512; i++) - { - // deterministic 'random' implementation - int n = origData[i] + m + 1; - if (n > 0xff) - n -= 0xff; - else if (n < 0) - n = 0xff + n; - - byte nByte = (byte)n; - - if (m == 0) - { - data.Add(origData[i]); - continue; - } - - if (i < weakArr[0]) - { - data.Add(origData[i]); - } - - else if (weakArr[1] > 0) - { - data.Add(nByte); - weakArr[1]--; - } - - else - { - data.Add(origData[i]); - } - } - } - - sec.SectorData = data.ToArray(); - sec.ActualDataByteLength = data.Count; - sec.ContainsMultipleWeakSectors = true; - } - } - else if (DetectAlkatraz(ref weakArr)) - { - Protection = ProtectionType.Alkatraz; - } - else if (DetectPaulOwens(ref weakArr)) - { - Protection = ProtectionType.PaulOwens; - } - else if (DetectHexagon(ref weakArr)) - { - Protection = ProtectionType.Hexagon; - } - else if (DetectShadowOfTheBeast()) - { - Protection = ProtectionType.ShadowOfTheBeast; - } - } - - /// - /// Detection routine for shadow of the beast game - /// Still cannot get this to work, but at least the game is detected - /// - public bool DetectShadowOfTheBeast() - { - if (DiskTracks[0].Sectors.Length != 9) - return false; - - var zeroSecs = DiskTracks[0].Sectors; - if (zeroSecs[0].SectorID is not 65 - || zeroSecs[1].SectorID is not 66 - || zeroSecs[2].SectorID is not 67 - || zeroSecs[3].SectorID is not 68 - || zeroSecs[4].SectorID is not 69 - || zeroSecs[5].SectorID is not 70 - || zeroSecs[6].SectorID is not 71 - || zeroSecs[7].SectorID is not 72 - || zeroSecs[8].SectorID is not 73) - { - return false; - } - - var oneSecs = DiskTracks[1].Sectors; - - if (oneSecs.Length != 8) - return false; - - if (oneSecs[0].SectorID is not 17 - || oneSecs[1].SectorID is not 18 - || oneSecs[2].SectorID is not 19 - || oneSecs[3].SectorID is not 20 - || oneSecs[4].SectorID is not 21 - || oneSecs[5].SectorID is not 22 - || oneSecs[6].SectorID is not 23 - || oneSecs[7].SectorID is not 24) - { - return false; - } - - return true; - } - - /// - /// Detect speedlock weak sector - /// - public bool DetectSpeedlock(ref int[] weak) - { - // SPEEDLOCK NOTES (-asni 2018-05-01) - // --------------------------------- - // Speedlock is one of the more common +3 disk protections and there are a few different versions - // Usually, track 0 sector 1 (ID 2) has data CRC errors that result in certain bytes returning a different value every time they are read - // Speedlock will generally read this track a number of times during the load process - // and if the correct bytes are not different between reads, the load fails - - // always must have track 0 containing 9 sectors - if (DiskTracks[0].Sectors.Length != 9) - return false; - - // check for SPEEDLOCK ident in sector 0 - string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[0].SectorData, 0, DiskTracks[0].Sectors[0].SectorData.Length); - if (!ident.ContainsIgnoreCase("SPEEDLOCK")) return false; - - // check for correct sector 0 lengths - if (DiskTracks[0].Sectors[0] is not { SectorSize: 2, SectorData.Length: >= 0x200 }) return false; - - // sector[1] (SectorID 2) contains the weak sectors - // check for correct sector 1 lengths - if (DiskTracks[0].Sectors[1] is not { SectorSize: 2, SectorData.Length: >= 0x200 } sec) return false; - - // secID 2 needs a CRC error - //if (!(sec.Status1.Bit(5) || sec.Status2.Bit(5))) - //return false; - - // check for filler - bool startFillerFound = true; - for (int i = 0; i < 250; i++) - { - if (sec.SectorData[i] != sec.SectorData[i + 1]) - { - startFillerFound = false; - break; - } - } - - if (!startFillerFound) - { - weak[0] = 0; - weak[1] = 0x200; - } - else - { - weak[0] = 0x150; - weak[1] = 0x20; - } - - return true; - } - - /// - /// Detect Alkatraz - /// - public bool DetectAlkatraz(ref int[] weak) - { - try - { - var data1 = DiskTracks[0].Sectors[0].SectorData; - var data2 = DiskTracks[0].Sectors[0].SectorData.Length; - } - catch (Exception) - { - return false; - } - - // check for ALKATRAZ ident in sector 0 - string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[0].SectorData, 0, DiskTracks[0].Sectors[0].SectorData.Length); - if (!ident.ContainsIgnoreCase("ALKATRAZ PROTECTION SYSTEM")) return false; - - // ALKATRAZ NOTES (-asni 2018-05-01) - // --------------------------------- - // Alkatraz protection appears to revolve around a track on the disk with 18 sectors, - // (track position is not consistent) with the sector ID info being incorrect: - // TrackID is consistent between the sectors although is usually high (233, 237 etc) - // SideID is fairly random looking but with all IDs being even - // SectorID is also fairly random looking but contains both odd and even numbers - // - // There doesnt appear to be any CRC errors in this track, but the sector size is always 1 (256 bytes) - // Each sector contains different filler byte - // Once track 0 is loaded the CPU completely reads all the sectors in this track one-by-one. - // Data transferred during execution must be correct, also result ST0, ST1 and ST2 must be 64, 128 and 0 respectively - - // Immediately following this track are a number of tracks and sectors with a DAM set. - // These are all read in sector by sector - // Again, Alkatraz appears to require that ST0, ST1, and ST2 result bytes are set to 64, 128 and 0 respectively - // (so the CM in ST2 needs to be reset) - - return true; - } - - /// - /// Detect Paul Owens - /// - public bool DetectPaulOwens(ref int[] weak) - { - try - { - var data1 = DiskTracks[0].Sectors[2].SectorData; - var data2 = DiskTracks[0].Sectors[2].SectorData.Length; - } - catch (Exception) - { - return false; - } - - // check for PAUL OWENS ident in sector 2 - string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[2].SectorData, 0, DiskTracks[0].Sectors[2].SectorData.Length); - if (!ident.ContainsIgnoreCase("PAUL OWENS")) return false; - - // Paul Owens Disk Protection Notes (-asni 2018-05-01) - // --------------------------------------------------- - // - // This scheme looks a little similar to Alkatraz with incorrect sector ID info in many places - // and deleted address marks (although these do not seem to show the strict relience on removing the CM mark from ST2 result that Alkatraz does) - // There are also data CRC errors but these don't look to be read more than once/checked for changes during load - // Main identifiers: - // - // * There are more than 10 cylinders - // * Cylinder 1 has no sector data - // * The sector ID infomation in most cases contains incorrect track IDs - // * Tracks 0 (boot) and 5 appear to be pretty much the only tracks that do not have incorrect sector ID marks - - return true; - } - - /// - /// Detect Hexagon copy protection - /// - public bool DetectHexagon(ref int[] weak) - { - try - { - var data1 = DiskTracks[0].Sectors.Length; - var data2 = DiskTracks[0].Sectors[8].ActualDataByteLength; - var data3 = DiskTracks[0].Sectors[8].SectorData; - var data4 = DiskTracks[0].Sectors[8].SectorData.Length; - var data5 = DiskTracks[1].Sectors[0]; - } - catch (Exception) - { - return false; - } - - if (DiskTracks[0].Sectors.Length != 10 || DiskTracks[0].Sectors[8].ActualDataByteLength != 512) - return false; - - // check for Hexagon ident in sector 8 - string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[8].SectorData, 0, DiskTracks[0].Sectors[8].SectorData.Length); - if (ident.ContainsIgnoreCase("GON DISK PROT")) return true; - - // hexagon protection may not be labelled as such - var track = DiskTracks[1]; - var sector = track.Sectors[0]; - - if (sector.SectorSize == 6 && sector.Status1 == 0x20 && sector.Status2 == 0x60) - { - if (track.Sectors.Length == 1) - return true; - } - - - // Hexagon Copy Protection Notes (-asni 2018-05-01) - // --------------------------------------------------- - // none - - return false; - } - - /* - /// - /// Should be run at the end of the ParseDisk process - /// If speedlock is detected the flag is set in the disk image - /// - protected virtual void SpeedlockDetection() - { - - if (DiskTracks.Length == 0) - return; - - // check for speedlock copyright notice - string ident = Encoding.ASCII.GetString(DiskData, 0x100, 0x1400); - if (!ident.ContainsIgnoreCase("SPEEDLOCK")) - { - // speedlock not found - return; - } - - // get cylinder 0 - var cyl = DiskTracks[0]; - - // get sector with ID=2 - var sec = cyl.Sectors.Where(a => a.SectorID == 2).FirstOrDefault(); - - if (sec == null) - return; - - // check for already multiple weak copies - if (sec.ContainsMultipleWeakSectors || sec.SectorData.Length != 0x80 << sec.SectorSize) - return; - - // check for invalid crcs in sector 2 - if (sec.Status1.Bit(5) || sec.Status2.Bit(5)) - { - Protection = ProtectionType.Speedlock; - } - else - { - return; - } - - // we are going to create a total of 5 weak sector copies - // keeping the original copy - byte[] origData = sec.SectorData.ToArray(); - List data = new(); //TODO pretty sure the length and indices here are known in advance and this can just be an array --yoshi - - for (int i = 0; i < 6; i++) - { - for (int s = 0; s < origData.Length; s++) - { - if (i == 0) - { - data.Add(origData[s]); - continue; - } - - // deterministic 'random' implementation - int n = origData[s] + i + 1; - if (n > 0xff) - n = n - 0xff; - else if (n < 0) - n = 0xff + n; - - byte nByte = (byte)n; - - if (s < 336) - { - // non weak data - data.Add(origData[s]); - } - else if (s < 511) - { - // weak data - data.Add(nByte); - } - else if (s == 511) - { - // final sector byte - data.Add(nByte); - } - else - { - // speedlock sector should not be more than 512 bytes - // but in case it is just do non weak - data.Add(origData[i]); - } - } - } - - // commit the sector data - sec.SectorData = data.ToArray(); - sec.ContainsMultipleWeakSectors = true; - sec.ActualDataByteLength = data.Count; - - } - */ - - /// - /// Returns the track count for the disk - /// - public virtual int GetTrackCount() - { - return DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; - } - - /// - /// Reads the current sector ID info - /// - public virtual CHRN ReadID(byte trackIndex, byte side, int sectorIndex) - { - if (side != 0) - return null; - - if (DiskTracks.Length <= trackIndex || trackIndex < 0) - { - // invalid track - wrap around - trackIndex = 0; - } - - var track = DiskTracks[trackIndex]; - - if (track.NumberOfSectors <= sectorIndex) - { - // invalid sector - wrap around - sectorIndex = 0; - } - - var sector = track.Sectors[sectorIndex]; - - CHRN chrn = new CHRN(); - - chrn.C = sector.TrackNumber; - chrn.H = sector.SideNumber; - chrn.R = sector.SectorID; - - // wrap around for N > 7 - if (sector.SectorSize > 7) - { - chrn.N = (byte)(sector.SectorSize - 7); - } - else if (sector.SectorSize < 0) - { - chrn.N = 0; - } - else - { - chrn.N = sector.SectorSize; - } - - chrn.Flag1 = (byte)(sector.Status1 & 0x25); - chrn.Flag2 = (byte)(sector.Status2 & 0x61); - - chrn.DataBytes = sector.ActualData; - - return chrn; - } - - /// - /// State serialization routines - /// - public abstract void SyncState(Serializer ser); - - - public class Header - { - public string DiskIdent { get; set; } - public string DiskCreatorString { get; set; } - public byte NumberOfTracks { get; set; } - public byte NumberOfSides { get; set; } - public int[] TrackSizes { get; set; } - } - - public class Track - { - public string TrackIdent { get; set; } - public byte TrackNumber { get; set; } - public byte SideNumber { get; set; } - public byte DataRate { get; set; } - public byte RecordingMode { get; set; } - public byte SectorSize { get; set; } - public byte NumberOfSectors { get; set; } - public byte GAP3Length { get; set; } - public byte FillerByte { get; set; } - public virtual Sector[] Sectors { get; set; } - - public virtual byte TrackType { get; set; } - public virtual int TLEN { get; set; } - public virtual int CLEN => TLEN / 8 + (TLEN % 8 / 7) / 8; - public virtual byte[] TrackData { get; set; } - - /// - /// Presents a contiguous byte array of all sector data for this track - /// (including any multiple weak/random data) - /// - public virtual byte[] TrackSectorData - => CollectionExtensions.ConcatArrays(Sectors.Select(static sec => sec.ActualData).ToArray()); - } - - public class Sector - { - public virtual byte TrackNumber { get; set; } - public virtual byte SideNumber { get; set; } - public virtual byte SectorID { get; set; } - public virtual byte SectorSize { get; set; } - public virtual byte Status1 { get; set; } - public virtual byte Status2 { get; set; } - public virtual int ActualDataByteLength { get; set; } - public virtual byte[] SectorData { get; set; } - public virtual bool ContainsMultipleWeakSectors { get; set; } - - public int WeakReadIndex = 0; - - public void SectorReadCompleted() - { - if (ContainsMultipleWeakSectors) - WeakReadIndex++; - } - - public int DataLen - { - get - { - if (!ContainsMultipleWeakSectors) - { - return ActualDataByteLength; - } - - return ActualDataByteLength / (ActualDataByteLength / (0x80 << SectorSize)); - } - } - - - public int RandSecCounter = 0; - - public byte[] ActualData - { - get - { - if (!ContainsMultipleWeakSectors) - { - // check whether filler bytes are needed - int size = 0x80 << SectorSize; - if (size > ActualDataByteLength) - { - var buf = new byte[SectorData.Length + size - ActualDataByteLength]; - SectorData.AsSpan().CopyTo(buf); -// SectorData.AsSpan(start: 0, length: buf.Length - SectorData.Length) -// .CopyTo(buf.AsSpan(start: SectorData.Length)); - buf.AsSpan(start: SectorData.Length).Fill(SectorData[SectorData.Length - 1]); - return buf; - } - - return SectorData; - } - else - { - // weak read neccessary - int copies = ActualDataByteLength / (0x80 << SectorSize); - - // handle index wrap-around - if (WeakReadIndex > copies - 1) - WeakReadIndex = copies - 1; - - // get the sector data based on the current weakreadindex - int step = WeakReadIndex * (0x80 << SectorSize); - byte[] res = new byte[(0x80 << SectorSize)]; - Array.Copy(SectorData, step, res, 0, 0x80 << SectorSize); - return res; - - /* - int copies = ActualDataByteLength / (0x80 << SectorSize); - var r = new Random(Seed: 4) // chosen by fair dice roll. guaranteed to be random. - .Next(0, copies - 1); - int step = r * (0x80 << SectorSize); - byte[] res = new byte[(0x80 << SectorSize)]; - Array.Copy(SectorData, step, res, 0, 0x80 << SectorSize); - return res; - */ - } - } - } - - public CHRN SectorIDInfo => - new CHRN - { - C = TrackNumber, - H = SideNumber, - R = SectorID, - N = SectorSize, - Flag1 = Status1, - Flag2 = Status2, - }; - } - } - - /// - /// Defines the type of speedlock detection found - /// - public enum ProtectionType - { - None, - Speedlock, - Alkatraz, - Hexagon, - Frontier, - PaulOwens, - ShadowOfTheBeast - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/IPFFormat/IPFFloppyDisk.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/IPFFormat/IPFFloppyDisk.cs deleted file mode 100644 index 92ff7cf96a4..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/IPFFormat/IPFFloppyDisk.cs +++ /dev/null @@ -1,447 +0,0 @@ -using BizHawk.Common; -using BizHawk.Common.NumberExtensions; - -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BizHawk.Common.StringExtensions; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - public class IPFFloppyDisk : FloppyDisk - { - /// - /// The format type - /// - public override DiskType DiskFormatType => DiskType.IPF; - - /// - /// Attempts to parse incoming disk data - /// - /// - /// TRUE: disk parsed - /// FALSE: unable to parse disk - /// - public override bool ParseDisk(byte[] data) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 16); - - if (!ident.ContainsIgnoreCase("CAPS")) - { - // incorrect format - return false; - } - - int pos = 0; - - List blocks = new List(); - - while (pos < data.Length) - { - try - { - var block = IPFBlock.ParseNextBlock(ref pos, this, data, blocks); - - if (block == null) - { - // EOF - break; - } - - if (block.RecordType == RecordHeaderType.None) - { - // unknown block - } - - blocks.Add(block); - } - catch (Exception ex) - { - var e = ex.ToString(); - } - } - - // now process the blocks - var infoBlock = blocks.Find(static a => a.RecordType == RecordHeaderType.INFO); - var IMGEblocks = blocks.Where(a => a.RecordType == RecordHeaderType.IMGE).ToList(); - var DATAblocks = blocks.Where(static block => block.RecordType is RecordHeaderType.DATA).ToArray(); - - DiskHeader.NumberOfTracks = (byte)(IMGEblocks.Count); - DiskHeader.NumberOfSides = (byte)(infoBlock.INFOmaxSide + 1); - DiskTracks = new Track[DiskHeader.NumberOfTracks]; - - for (int t = 0; t < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; t++) - { - // each imge block represents one track - var img = IMGEblocks[t]; - DiskTracks[t] = new Track(); - var trk = DiskTracks[t]; - - var blockCount = img.IMGEblockCount; - var dataBlock = DATAblocks.FirstOrDefault(a => a.DATAdataKey == img.IMGEdataKey); - - trk.SideNumber = (byte)img.IMGEside; - trk.TrackNumber = (byte)img.IMGEtrack; - - trk.Sectors = new Sector[blockCount]; - - // process data block descriptors - int p = 0; - for (int d = 0; d < blockCount; d++) - { - var extraDataAreaStart = 32 * blockCount; - trk.Sectors[d] = new Sector(); - var sector = trk.Sectors[d]; - - int dataBits = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - int gapBits = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - int dataBytes; - int gapBytes; - int gapOffset; - int cellType; - if (infoBlock.INFOencoderType == 1) - { - dataBytes = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - gapBytes = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - } - else if (infoBlock.INFOencoderType == 2) - { - gapOffset = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - cellType = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - } - int encoderType = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - int? blockFlags = null; - if (infoBlock.INFOencoderType == 2) - { - blockFlags = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); - } - p += 4; - - int gapDefault = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - int dataOffset = MediaConverter.GetBEInt32(dataBlock.DATAextraDataRaw, p); p += 4; - - // gap stream elements - if (infoBlock.INFOencoderType == 2 && gapBits != 0 && blockFlags != null) - { - if (!blockFlags.Value.Bit(1) && !blockFlags.Value.Bit(0)) - { - // no gap stream - } - if (!blockFlags.Value.Bit(1) && blockFlags.Value.Bit(0)) - { - // Forward gap stream list only - } - if (blockFlags.Value.Bit(1) && !blockFlags.Value.Bit(0)) - { - // Backward gap stream list only - } - if (blockFlags.Value.Bit(1) && blockFlags.Value.Bit(0)) - { - // Forward and Backward stream lists - } - } - - // data stream elements - if (dataBits != 0) - { - var dsLocation = dataOffset; - - for (; ; ) - { - byte dataHead = dataBlock.DATAextraDataRaw[dsLocation++]; - if (dataHead == 0) - { - // end of data stream list - break; - } - - var sampleSize = ((dataHead & 0xE0) >> 5); - var dataType = dataHead & 0x1F; - byte[] dSize = new byte[sampleSize]; - Array.Copy(dataBlock.DATAextraDataRaw, dsLocation, dSize, 0, sampleSize); - var dataSize = MediaConverter.GetBEInt32FromByteArray(dSize); - dsLocation += dSize.Length; - int dataLen; - var dataStream = Array.Empty(); - - if (blockFlags != null && blockFlags.Value.Bit(2)) - { - // bits - if (dataType != 5) - { - dataLen = dataSize / 8; - if (dataSize % 8 != 0) - { - // bits left over - } - dataStream = new byte[dataLen]; - Array.Copy(dataBlock.DATAextraDataRaw, dsLocation, dataStream, 0, dataLen); - } - } - else - { - // bytes - if (dataType != 5) - { - dataStream = new byte[dataSize]; - Array.Copy(dataBlock.DATAextraDataRaw, dsLocation, dataStream, 0, dataSize); - } - } - - // dataStream[] now contains the data - switch (dataType) - { - // SYNC - case 1: - break; - // DATA - case 2: - if (dataStream.Length == 7) - { - // ID - // first byte IAM - sector.TrackNumber = dataStream[1]; - sector.SideNumber = dataStream[2]; - sector.SectorID = dataStream[3]; - sector.SectorSize = dataStream[4]; - } - else if (dataStream.Length > 255) - { - // DATA - // first byte DAM - if (dataStream[0] == 0xF8) - { - // deleted address mark - //sector.Status1 - } - sector.SectorData = new byte[dataStream.Length - 1 - 2]; - Array.Copy(dataStream, 1, sector.SectorData, 0, dataStream.Length - 1 - 2); - } - break; - // GAP - case 3: - break; - // RAW - case 4: - break; - // FUZZY - case 5: - break; - default: - break; - } - - - dsLocation += dataStream.Length; - } - } - } - } - - return true; - } - - public class IPFBlock - { - public RecordHeaderType RecordType; - public int BlockLength; - public int CRC; - public byte[] RawBlockData; - public int StartPos; - - public int INFOmediaType; - public int INFOencoderType; - public int INFOencoderRev; - public int INFOfileKey; - public int INFOfileRev; - public int INFOorigin; - public int INFOminTrack; - public int INFOmaxTrack; - public int INFOminSide; - public int INFOmaxSide; - public int INFOcreationDate; - public int INFOcreationTime; - public int INFOplatform1; - public int INFOplatform2; - public int INFOplatform3; - public int INFOplatform4; - public int INFOdiskNumber; - public int INFOcreatorId; - - public int IMGEtrack; - public int IMGEside; - public int IMGEdensity; - public int IMGEsignalType; - public int IMGEtrackBytes; - public int IMGEstartBytePos; - public int IMGEstartBitPos; - public int IMGEdataBits; - public int IMGEgapBits; - public int IMGEtrackBits; - public int IMGEblockCount; - public int IMGEencoderProcess; - public int IMGEtrackFlags; - public int IMGEdataKey; - - public int DATAlength; - public int DATAbitSize; - public int DATAcrc; - public int DATAdataKey; - public byte[] DATAextraDataRaw; - - public static IPFBlock ParseNextBlock(ref int startPos, FloppyDisk disk, byte[] data, List blockCollection) - { - IPFBlock ipf = new IPFBlock(); - ipf.StartPos = startPos; - - if (startPos >= data.Length) - { - // EOF - return null; - } - - // assume the startPos passed in is actually the start of a new block - // look for record header ident - string ident = Encoding.ASCII.GetString(data, startPos, 4); - startPos += 4; - try - { - ipf.RecordType = (RecordHeaderType)Enum.Parse(typeof(RecordHeaderType), ident); - } - catch - { - ipf.RecordType = RecordHeaderType.None; - } - - // setup for actual block size - ipf.BlockLength = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.CRC = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.RawBlockData = new byte[ipf.BlockLength]; - Array.Copy(data, ipf.StartPos, ipf.RawBlockData, 0, ipf.BlockLength); - - switch (ipf.RecordType) - { - // Nothing to process / unknown - // just move ahead - case RecordHeaderType.CAPS: - case RecordHeaderType.TRCK: - case RecordHeaderType.DUMP: - case RecordHeaderType.CTEI: - case RecordHeaderType.CTEX: - default: - startPos = ipf.StartPos + ipf.BlockLength; - break; - - // INFO block - case RecordHeaderType.INFO: - // INFO header is followed immediately by an INFO block - ipf.INFOmediaType = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOencoderType = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOencoderRev = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOfileKey = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOfileRev = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOorigin = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOminTrack = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOmaxTrack = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOminSide = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOmaxSide = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOcreationDate = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOcreationTime = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOplatform1 = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOplatform2 = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOplatform3 = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOplatform4 = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOdiskNumber = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.INFOcreatorId = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - startPos += 12; // reserved - break; - - case RecordHeaderType.IMGE: - ipf.IMGEtrack = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEside = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEdensity = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEsignalType = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEtrackBytes = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEstartBytePos = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEstartBitPos = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEdataBits = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEgapBits = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEtrackBits = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEblockCount = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEencoderProcess = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEtrackFlags = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.IMGEdataKey = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - startPos += 12; // reserved - break; - - case RecordHeaderType.DATA: - ipf.DATAlength = MediaConverter.GetBEInt32(data, startPos); - if (ipf.DATAlength == 0) - { - ipf.DATAextraDataRaw = Array.Empty(); - ipf.DATAlength = 0; - } - else - { - ipf.DATAextraDataRaw = new byte[ipf.DATAlength]; - } - startPos += 4; - ipf.DATAbitSize = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.DATAcrc = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - ipf.DATAdataKey = MediaConverter.GetBEInt32(data, startPos); startPos += 4; - - if (ipf.DATAlength != 0) - { - Array.Copy(data, startPos, ipf.DATAextraDataRaw, 0, ipf.DATAlength); - } - - startPos += ipf.DATAlength; - break; - } - - return ipf; - } - } - - public enum RecordHeaderType - { - None, - CAPS, - DUMP, - DATA, - TRCK, - INFO, - IMGE, - CTEI, - CTEX, - } - - - /// - /// State serlialization - /// - public override void SyncState(Serializer ser) - { - ser.BeginSection("Plus3FloppyDisk"); - - ser.Sync(nameof(CylinderCount), ref CylinderCount); - ser.Sync(nameof(SideCount), ref SideCount); - ser.Sync(nameof(BytesPerTrack), ref BytesPerTrack); - ser.Sync(nameof(WriteProtected), ref WriteProtected); - ser.SyncEnum(nameof(Protection), ref Protection); - - ser.Sync(nameof(DirtyData), ref DirtyData); - if (DirtyData) - { - //TODO - } - - // sync deterministic track and sector counters - ser.Sync(nameof(_randomCounter), ref _randomCounter); - RandomCounter = _randomCounter; - - ser.EndSection(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/UDIFormat/UDI1_0FloppyDisk.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/UDIFormat/UDI1_0FloppyDisk.cs deleted file mode 100644 index 84f4c450cf8..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Disk/UDIFormat/UDI1_0FloppyDisk.cs +++ /dev/null @@ -1,214 +0,0 @@ -using BizHawk.Common; - -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BizHawk.Common.StringExtensions; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - public class UDI1_0FloppyDisk : FloppyDisk - { - /// - /// The format type - /// - public override DiskType DiskFormatType => DiskType.UDI; - - /// - /// Attempts to parse incoming disk data - /// - /// - /// TRUE: disk parsed - /// FALSE: unable to parse disk - /// - public override bool ParseDisk(byte[] data) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 4); - - if (!ident.StartsWithOrdinal("UDI!") && !ident.StartsWithOrdinal("udi!")) - { - // incorrect format - return false; - } - - if (data[0x08] != 0) - { - // wrong version - return false; - } - - if (ident == "udi!") - { - // cant handle compression yet - return false; - } - - DiskHeader.DiskIdent = ident; - DiskHeader.NumberOfTracks = (byte)(data[0x09] + 1); - DiskHeader.NumberOfSides = (byte)(data[0x0A] + 1); - - DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; - - int fileSize = MediaConverter.GetInt32(data, 4); // not including the final 4-byte checksum - - // ignore extended header - var extHdrSize = MediaConverter.GetInt32(data, 0x0C); - int pos = 0x10 + extHdrSize; - - // process track information - for (int t = 0; t < DiskHeader.NumberOfTracks; t++) - { - DiskTracks[t] = new UDIv1Track - { - TrackNumber = (byte) t, - SideNumber = 0, - TrackType = data[pos++], - TLEN = MediaConverter.GetWordValue(data, pos) - }; - pos += 2; - DiskTracks[t].TrackData = new byte[DiskTracks[t].TLEN + DiskTracks[t].CLEN]; - Array.Copy(data, pos, DiskTracks[t].TrackData, 0, DiskTracks[t].TLEN + DiskTracks[t].CLEN); - pos += DiskTracks[t].TLEN + DiskTracks[t].CLEN; - } - - return true; - } - - /// - /// Takes a double-sided disk byte array and converts into 2 single-sided arrays - /// - public static bool SplitDoubleSided(byte[] data, List results) - { - // look for standard magic string - string ident = Encoding.ASCII.GetString(data, 0, 4); - - if (!ident.StartsWithOrdinal("UDI!") && !ident.StartsWithOrdinal("udi!")) - { - // incorrect format - return false; - } - - if (data[0x08] != 0) - { - // wrong version - return false; - } - - if (ident == "udi!") - { - // cant handle compression yet - return false; - } - - byte[] S0 = new byte[data.Length]; - byte[] S1 = new byte[data.Length]; - - // header - var extHdr = MediaConverter.GetInt32(data, 0x0C); - Array.Copy(data, 0, S0, 0, 0x10 + extHdr); - Array.Copy(data, 0, S1, 0, 0x10 + extHdr); - // change side number - S0[0x0A] = 0; - S1[0x0A] = 0; - - int pos = 0x10 + extHdr; - int fileSize = MediaConverter.GetInt32(data, 4); // not including the final 4-byte checksum - - int s0Pos = pos; - int s1Pos = pos; - - // process track information - for (int t = 0; t < (data[0x09] + 1) * 2; t++) - { - var TLEN = MediaConverter.GetWordValue(data, pos + 1); - var CLEN = TLEN / 8 + (TLEN % 8 / 7) / 8; - var blockSize = TLEN + CLEN + 3; - - // 2 sided image: side 0 tracks will all have t as an even number - try - { - if (t == 0 || t % 2 == 0) - { - Array.Copy(data, pos, S0, s0Pos, blockSize); - s0Pos += blockSize; - } - else - { - Array.Copy(data, pos, S1, s1Pos, blockSize); - s1Pos += blockSize; - } - } - catch (Exception) - { - // ignore - } - - - pos += blockSize; - } - - // skip checkum bytes for now - - byte[] s0final = new byte[s0Pos]; - byte[] s1final = new byte[s1Pos]; - Array.Copy(S0, 0, s0final, 0, s0Pos); - Array.Copy(S1, 0, s1final, 0, s1Pos); - - results.Add(s0final); - results.Add(s1final); - - return true; - } - - public class UDIv1Track : Track - { - /// - /// Parse the UDI TrackData byte[] array into sector objects - /// - public override Sector[] Sectors - { - get - { - List secs = new(); - var datas = TrackData.Skip(3).Take(TLEN).ToArray(); - var clocks = new BitArray(TrackData.Skip(3 + TLEN).Take(CLEN).ToArray()); - - return secs.ToArray(); - } - } - } - - public class UDIv1Sector : Sector - { - } - - - /// - /// State serlialization - /// - public override void SyncState(Serializer ser) - { - ser.BeginSection("Plus3FloppyDisk"); - - ser.Sync(nameof(CylinderCount), ref CylinderCount); - ser.Sync(nameof(SideCount), ref SideCount); - ser.Sync(nameof(BytesPerTrack), ref BytesPerTrack); - ser.Sync(nameof(WriteProtected), ref WriteProtected); - ser.SyncEnum(nameof(Protection), ref Protection); - - ser.Sync(nameof(DirtyData), ref DirtyData); - if (DirtyData) - { - //TODO - } - - // sync deterministic track and sector counters - ser.Sync(nameof(_randomCounter), ref _randomCounter); - RandomCounter = _randomCounter; - - ser.EndSection(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Snapshot/SZX/SZX.Methods.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Snapshot/SZX/SZX.Methods.cs index f1c11e00091..726883a9b2c 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Snapshot/SZX/SZX.Methods.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Snapshot/SZX/SZX.Methods.cs @@ -1,4 +1,4 @@ -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; @@ -14,7 +14,7 @@ public partial class SZX { private readonly SpectrumBase _machine; - private Z80A _cpu => _machine.CPU; + private Z80AOpt _cpu => _machine.CPU; private SZX(SpectrumBase machine) { diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs index e2cbedce039..2bcfba1f0f0 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs @@ -4,6 +4,7 @@ using System.Text; using BizHawk.Common.StringExtensions; +using BizHawk.Emulation.Cores.Tapes; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { @@ -49,23 +50,17 @@ public CswConverter(DatacorderDevice _tapeDevice) /// public override bool CheckType(byte[] data) { - // CSW Header - - // check whether this is a valid csw format file by looking at the identifier in the header - // (first 22 bytes of the file) - string ident = Encoding.ASCII.GetString(data, 0, 22); - - // version info - int majorVer = data[8]; - int minorVer = data[9]; + // CSW Header. The 22-byte signature is followed by a 0x1A terminator at 0x16. Guard against shorter + // files first: CheckType runs on every tape during type-detection, so a short file of another + // format must not throw here. + if (data == null || data.Length < 23) + return false; - if (!"COMPRESSED SQUARE WAVE".EqualsIgnoreCase(ident)) - { - // this is not a valid CSW format file + if (data[0x16] != 0x1A) return false; - } - return true; + string ident = Encoding.ASCII.GetString(data, 0, 22); + return "COMPRESSED SQUARE WAVE".EqualsIgnoreCase(ident); } /// @@ -180,6 +175,7 @@ 0x1D 0x00 BYTE[3] Reserved. _position += 3; + totalPulses = -1; // v1 has no pulse count; the buffer holds exactly the pulse data cswDataUncompressed = new byte[data.Length - _position]; if (compressionType == 1) @@ -209,19 +205,25 @@ 0x1D 0x00 BYTE[3] Reserved. var rate = (69888 * 50) / sampleRate; - for (int i = 0; i < cswDataUncompressed.Length;) + // each pulse is one sample-count byte; a zero byte means the next 4 bytes hold a 32-bit sample + // count. Period T-states = samples * (T-states per sample). For v2 stop after 'totalPulses' pulses + // (its buffer is padded by one byte, which would otherwise misfire the extended-pulse path and read + // past the end); v1 has no count so the whole buffer is consumed. + int idx = 0; + int emitted = 0; + while (idx < cswDataUncompressed.Length && (totalPulses < 0 || emitted < totalPulses)) { - int length = cswDataUncompressed[i++] * rate; - if (length == 0) + int samples = cswDataUncompressed[idx++]; + if (samples == 0 && idx + 4 <= cswDataUncompressed.Length) { - length = GetInt32(cswDataUncompressed, i) / rate; - i += 4; + samples = GetInt32(cswDataUncompressed, idx); + idx += 4; } - t.DataPeriods.Add(length); - + t.DataPeriods.Add(samples * rate); currLevel = !currLevel; t.DataLevels.Add(currLevel); + emitted++; } // add closing period diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs index df416ae6f03..6b4fb1a552b 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs @@ -4,6 +4,7 @@ using System.Text; using BizHawk.Common.StringExtensions; +using BizHawk.Emulation.Cores.Tapes; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { @@ -32,21 +33,11 @@ public sealed class PzxConverter : MediaConverter protected override string SelfTypeName => nameof(PzxConverter); - /// - /// Working list of generated tape data blocks - /// - private readonly IList _blocks = new List(); - /// /// Position counter /// private int _position = 0; - /// - /// Object to keep track of loops - this assumes there is only one loop at a time - /// - private readonly IList> _loopCounter = new List>(); - private readonly DatacorderDevice _datacorder; public PzxConverter(DatacorderDevice _tapeDevice) @@ -59,23 +50,13 @@ public PzxConverter(DatacorderDevice _tapeDevice) /// public override bool CheckType(byte[] data) { - // PZX Header - - // check whether this is a valid pzx format file by looking at the identifier in the header - // (first 4 bytes of the file) - string ident = Encoding.ASCII.GetString(data, 0, 4); - - // version info - int majorVer = data[8]; - int minorVer = data[9]; - - if (!"PZXT".EqualsIgnoreCase(ident)) - { - // this is not a valid PZX format file + // PZX Header - the "PZXT" tag is the first 4 bytes. Guard against shorter files: CheckType runs on + // every tape during type-detection, so a short file of another format must not throw here. + if (data == null || data.Length < 4) return false; - } - return true; + string ident = Encoding.ASCII.GetString(data, 0, 4); + return "PZXT".EqualsIgnoreCase(ident); } /// @@ -167,42 +148,42 @@ 6 ... ... ditto repeated until the end of the block t.InitialPulseLevel = false; bool pLevel = !t.InitialPulseLevel; - List pulses = new List(); + var pulses = new List<(int Count, int Duration)>(); while (pos < blockSize + 8) { - ushort[] p = new ushort[2]; - p[0] = 1; - p[1] = GetWordValue(b, pos); + int count = 1; + int duration = GetWordValue(b, pos); pos += 2; - if (p[1] > 0x8000) + // bit 15 of the first word => a repeat count (low 15 bits) is present; the duration follows + if (duration >= 0x8000) { - p[0] = (ushort)(p[1] & 0x7fff); - p[1] = GetWordValue(b, pos); + count = duration & 0x7FFF; + duration = GetWordValue(b, pos); pos += 2; } - if (p[1] >= 0x8000) + // bit 15 of the duration word => extended duration ((d & 0x7FFF) << 16) | next word. + // (kept as int, not ushort - the old code shifted a ushort left 16 and lost the high bits) + if (duration >= 0x8000) { - p[1] &= 0x7fff; - p[1] <<= 16; - p[1] |= GetWordValue(b, pos); + duration = ((duration & 0x7FFF) << 16) | GetWordValue(b, pos); pos += 2; } - pulses.Add(p); + pulses.Add((count, duration)); } // convert to tape block t.BlockDescription = BlockType.PULS; t.PauseInMS = 0; - foreach (var x in pulses) + foreach (var (count, duration) in pulses) { - for (int i = 0; i < x[0]; i++) + for (int i = 0; i < count; i++) { - t.DataPeriods.Add(x[1]); + t.DataPeriods.Add(duration); pLevel = !pLevel; t.DataLevels.Add(pLevel); } @@ -254,7 +235,9 @@ 8 u16[p0] s0 sequence of pulse durations encoding bit equa var p0 = b[pos++]; var p1 = b[pos++]; - for (int i = 0; i < p1; i++) + // s0 has p0 pulse durations, s1 has p1 (the s0 loop previously used p1 in error, + // mis-reading s0 and generating the wrong pulses whenever p0 != p1) + for (int i = 0; i < p0; i++) { var s = GetWordValue(b, pos); pos += 2; @@ -270,9 +253,15 @@ 8 u16[p0] s0 sequence of pulse durations encoding bit equa var dData = b.AsSpan(start: pos, length: (dCount + 7) >> 3/* == `ceil(dCount/8)` */); pos += dData.Length; + + // retain the raw data bytes so a trap/flash-loader can read the block directly + t.BlockData = dData.ToArray(); + + // emit exactly dCount bits (MSb first); the final byte may be partial + int bitsDone = 0; foreach (var by in dData) { - for (int i = 7; i >= 0; i--) + for (int i = 7; i >= 0 && bitsDone < dCount; i--, bitsDone++) { if (by.Bit(i)) { diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs index 1fcc30263f9..fc99ceb4387 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Text; +using BizHawk.Emulation.Cores.Tapes; + namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// @@ -267,14 +269,14 @@ A SCREEN$ file is regarded as a Code file with start address 16384 and length 69 // calculate the data periods for this block int pilotLength = 0; - // work out pilot length - if (blockdata[0] < 4) + // work out pilot length (spec: 8063 pulses for a header, flag byte < 128, else 3223 for data) + if (blockdata[0] < 128) { - pilotLength = 8064; + pilotLength = 8063; } else { - pilotLength = 3220; + pilotLength = 3223; } // create a list to hold the data periods diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs index 19b46b016f3..5edd759bf57 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Text; +using BizHawk.Emulation.Cores.Tapes; + namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { /// @@ -38,11 +40,6 @@ protected override string SelfTypeName /// private int _position = 0; - /// - /// Object to keep track of loops - this assumes there is only one loop at a time - /// - private readonly List> _loopCounter = new List>(); - /// /// The virtual cassette deck /// @@ -215,10 +212,23 @@ 0x09 20 BYTE TZX minor revision number ProcessBlock(); } - // final pause (some games require this to finish loading properly - eg Kong2) + // resolve control-flow blocks (jump / loop / call / return / select) into the final linear play + // list. Control markers are consumed here and do not appear in the played blocks. + ExpandControlFlow(); + + // final pause as its own dedicated block (some games require a trailing silence to finish loading - + // eg Kong2). Emitting it as a new block avoids depending on whichever block the field 't' last + // referenced (blocks that use a local 't', eg 0x15/0x19, would otherwise get the pause on the wrong block). + t = new TapeDataBlock + { + BlockID = 0x20, + BlockDescription = BlockType.Pause_or_Stop_the_Tape, + PauseInMS = 1000, + PauseInTStates = TranslatePause(1000), + }; pauseLen = 1000; - t.PauseInTStates = TranslatePause(1000); DoPause(); + _datacorder.DataBlocks.Add(t); /* debugging stuff StringBuilder export = new StringBuilder(); @@ -249,6 +259,103 @@ private void ToggleSignal() signal = !signal; } + /// + /// Resolves the control-flow blocks (jump / loop / call / return / select) into a linear list of + /// playable blocks. Walks the parsed blocks following the control flow - loops repeat, jumps redirect, + /// call/return follow the call list, select defaults to the first option - and rebuilds the datacorder's + /// block list from the blocks actually visited. A visit cap guards against a malformed infinite loop. + /// + private void ExpandControlFlow() + { + var raw = _datacorder.DataBlocks; + + // fast path: nothing to do if there are no control blocks + bool hasControl = raw.Exists(b => b.Command + is TapeCommand.JUMP or TapeCommand.LOOP_START or TapeCommand.LOOP_END + or TapeCommand.CALL_SEQUENCE or TapeCommand.RETURN_FROM_SEQUENCE or TapeCommand.SELECT_BLOCK); + if (!hasControl) + return; + + var outList = new List(); + int loopStart = -1; + int loopRemaining = 0; + var callStack = new Stack<(int ReturnIndex, int[] Offsets, int CallBase, int NextCall)>(); + + int i = 0; + int guard = 0; + const int Cap = 500000; + + while (i >= 0 && i < raw.Count && guard++ < Cap) + { + var b = raw[i]; + switch (b.Command) + { + case TapeCommand.LOOP_START: + loopStart = i + 1; + loopRemaining = b.ControlValue; + i++; + break; + case TapeCommand.LOOP_END: + if (loopRemaining > 1 && loopStart >= 0) + { + loopRemaining--; + i = loopStart; + } + else + { + i++; + } + break; + case TapeCommand.JUMP: + // signed relative offset; 'jump 0' (loop forever) is treated as 'next block' + i += b.ControlValue == 0 ? 1 : b.ControlValue; + break; + case TapeCommand.CALL_SEQUENCE: + if (b.ControlOffsets is { Length: > 0 }) + { + callStack.Push((i + 1, b.ControlOffsets, i, 0)); + i += b.ControlOffsets[0]; + } + else + { + i++; + } + break; + case TapeCommand.RETURN_FROM_SEQUENCE: + if (callStack.Count > 0) + { + var f = callStack.Pop(); + int next = f.NextCall + 1; + if (next < f.Offsets.Length) + { + callStack.Push((f.ReturnIndex, f.Offsets, f.CallBase, next)); + i = f.CallBase + f.Offsets[next]; + } + else + { + i = f.ReturnIndex; + } + } + else + { + i++; + } + break; + case TapeCommand.SELECT_BLOCK: + // default to the first selection (a full interactive menu is a front-end concern) + i += b.ControlOffsets is { Length: > 0 } ? b.ControlOffsets[0] : 1; + break; + default: + outList.Add(b); + i++; + break; + } + } + + _datacorder.DataBlocks.Clear(); + _datacorder.DataBlocks.AddRange(outList); + } + /// /// Processes a TZX block /// @@ -419,9 +526,8 @@ This block must be replayed with the standard Spectrum ROM timing values - see t byte[] blockdata = new byte[blockLen]; blockdata = data.Skip(_position).Take(blockLen).ToArray(); - // pilot count needs to be ascertained from flag byte - //pilotCount = blockdata[0] < 128 ? 8063 : 3222; // 3223; - pilotCount = blockdata[0] == 0 ? 8064 : 3219; + // pilot count is ascertained from the flag byte (spec: 8063 pulses if flag < 128, else 3223) + pilotCount = blockdata[0] < 128 ? 8063 : 3223; pilotToneLength = 2168; // 2133;// 2168; sync1PulseLength = 667; // 632; // 667; @@ -820,7 +926,8 @@ The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sam Please, if you can, don't use other sampling frequencies. Please use this block only if you cannot use any other block. */ - TapeDataBlock t = new TapeDataBlock + // use the field 't' (not a local) so DoPause appends the pause to THIS block + t = new TapeDataBlock { BlockID = 0x15, BlockDescription = BlockType.Direct_Recording @@ -880,7 +987,6 @@ The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sam } blockLen--; - _position++; } // pause processing @@ -922,43 +1028,56 @@ This block contains a sequence of raw pulses encoded in CSW format v2 (Compresse blockLen = GetInt32(data, _position); _position += 4; - t.PauseInMS = GetWordValue(data, _position); + // pause after this block (ms) - assign pauseLen so DoPause and PauseInTStates use the right value + pauseLen = GetWordValue(data, _position); + t.PauseInMS = pauseLen; t.PauseInTStates = TranslatePause(pauseLen); - _position += 2; - int sampleRate = data[_position++] << 16 | data[_position++] << 8 | data[_position++]; + // sampling rate is a 3-byte little-endian value + int sampleRate = data[_position] | (data[_position + 1] << 8) | (data[_position + 2] << 16); + _position += 3; + byte compType = data[_position++]; int pulses = GetInt32(data, _position); _position += 4; int dataLen = blockLen - 10; - // build source array + // copy the raw (RLE or Z-RLE compressed) CSW data out of the block, then decode it byte[] src = new byte[dataLen]; + Array.Copy(data, _position, src, 0, dataLen); // build destination array byte[] dest = new byte[pulses + 1]; // process the CSW data CswConverter.ProcessCSWV2(src, ref dest, compType, pulses); - // create the periods + // create the periods (and their alternating levels) var rate = (69888 * 50) / sampleRate; - for (int i = 0; i < dest.Length;) + // decode exactly 'pulses' pulses (iterating by buffer length would read the +1 padding byte and + // misfire the extended-pulse path). Each pulse is one sample-count byte; a zero byte means the next + // 4 bytes hold a 32-bit sample count. Period T-states = samples * (T-states per sample). + int p = 0; + for (int decoded = 0; decoded < pulses && p < dest.Length; decoded++) { - int length = dest[i++] * rate; - if (length == 0) + int samples = dest[p++]; + if (samples == 0 && p + 4 <= dest.Length) { - length = GetInt32(dest, i) / rate; - i += 4; + samples = GetInt32(dest, p); + p += 4; } - t.DataPeriods.Add(length); + t.DataPeriods.Add(samples * rate); + ToggleSignal(); + t.DataLevels.Add(signal); } // add closing period t.DataPeriods.Add((69888 * 50) / 10); + ToggleSignal(); + t.DataLevels.Add(signal); _position += dataLen; @@ -1096,24 +1215,13 @@ For simplicity reasons don't nest loop blocks! */ t = new TapeDataBlock(); t.BlockID = 0x24; t.BlockDescription = BlockType.Loop_Start; - - // loop should start from the next block - int loopStart = _datacorder.DataBlocks.Count + 1; - - int numberOfRepetitions = GetWordValue(data, _position); - - // update loop counter - _loopCounter.Add( - new KeyValuePair( - loopStart, - numberOfRepetitions)); - - // update description - //t.BlockDescription = "[LOOP START - " + numberOfRepetitions + " times]"; - t.PauseInMS = 0; t.PauseInTStates = 0; + // repetition count; the loop body is repeated during control-flow expansion + t.Command = TapeCommand.LOOP_START; + t.ControlValue = GetWordValue(data, _position); + // add to tape _datacorder.DataBlocks.Add(t); @@ -1139,38 +1247,14 @@ This block has no body. */ t.BlockID = 0x25; t.DataPeriods = new List(); t.BlockDescription = BlockType.Loop_End; + t.PauseInMS = 0; + t.PauseInTStates = 0; - // get the most recent loop info - var loop = _loopCounter.LastOrDefault(); - - int loopStart = loop.Key; - int numberOfRepetitions = loop.Value; - - if (numberOfRepetitions == 0) - { - return; - } - - // get the number of blocks to loop - int blockCnt = _datacorder.DataBlocks.Count - loopStart; - - // loop through each group to repeat - for (int b = 0; b < numberOfRepetitions; b++) - { - TapeDataBlock repeater = new TapeDataBlock(); - //repeater.BlockDescription = "[LOOP REPEAT - " + (b + 1) + "]"; - repeater.DataPeriods = new List(); - - // add the repeat block - _datacorder.DataBlocks.Add(repeater); + // the loop body (between loop start and here) is repeated during control-flow expansion + t.Command = TapeCommand.LOOP_END; - // now iterate through and add the blocks to be repeated - for (int i = 0; i < blockCnt; i++) - { - var block = _datacorder.DataBlocks[loopStart + i]; - _datacorder.DataBlocks.Add(block); - } - } + // add to tape + _datacorder.DataBlocks.Add(t); // ready for next block ZeroVars(); @@ -1233,13 +1317,10 @@ This block sets the current signal level to the specified value (high or low). I t.PauseInMS = 0; t.PauseInTStates = 0; - // we already flip the signal *before* adding to the buffer elsewhere - // so set the opposite level specified in this block + // set the current signal level (spec: 0 = low, 1 = high). 'signal' holds the current level and the + // next block's first pulse edges away from it (matching how a pause leaves the level). byte signalLev = data[_position + 4]; - if (signalLev == 0) - signal = true; - else - signal = false; + signal = signalLev != 0; // add to tape _datacorder.DataBlocks.Add(t); @@ -1628,17 +1709,22 @@ then goes back to the next block. Because more than one call can be normally use sequences and vice versa. The value is relative for the obvious reasons - so that you can add some blocks in the beginning of the file without disturbing the call values. Please take a look at 'Jump To Block' for reference on the values. */ - // block processing not implemented for this - just gets added for informational purposes only t = new TapeDataBlock(); t.BlockID = 0x26; t.DataPeriods = new List(); t.BlockDescription = BlockType.Call_Sequence; - - int blockSize = 2 + 2 * GetWordValue(data, _position); - t.PauseInMS = 0; t.PauseInTStates = 0; + // signed relative offsets of the blocks to call, resolved during control-flow expansion + int numCalls = GetWordValue(data, _position); + int blockSize = 2 + 2 * numCalls; + var callOffsets = new int[numCalls]; + for (int c = 0; c < numCalls; c++) + callOffsets[c] = (short)GetWordValue(data, _position + 2 + c * 2); + t.Command = TapeCommand.CALL_SEQUENCE; + t.ControlOffsets = callOffsets; + // add to tape _datacorder.DataBlocks.Add(t); @@ -1660,13 +1746,15 @@ This block indicates the end of the Called Sequence. The next block played will if the Call block had multiple calls). Again, this block has no body. */ - // block processing not implemented for this - just gets added for informational purposes only t = new TapeDataBlock(); t.BlockID = 0x27; t.BlockDescription = BlockType.Return_From_Sequence; t.PauseInMS = 0; t.PauseInTStates = 0; + // resolved during control-flow expansion (returns into the calling sequence) + t.Command = TapeCommand.RETURN_FROM_SEQUENCE; + // add to tape _datacorder.DataBlocks.Add(t); @@ -1699,16 +1787,29 @@ one of the parts and the utility/emulator will start loading from that block. Fo separate Trainer or when it is a multiload. Of course, to make some use of it the emulator/utility has to show a menu with the selections when it encounters such a block. All offsets are relative signed words. */ - // block processing not implemented for this - just gets added for informational purposes only t = new TapeDataBlock(); t.BlockID = 0x28; t.DataPeriods = new List(); t.BlockDescription = BlockType.Select_Block; + t.PauseInMS = 0; + t.PauseInTStates = 0; int blockSize = 2 + GetWordValue(data, _position); - t.PauseInMS = 0; - t.PauseInTStates = 0; + // parse the selection offsets (each: WORD signed offset + BYTE descLen + description). Expansion + // defaults to the first selection - a full interactive menu is a front-end concern. + int selPos = _position + 2; + int numSel = data[selPos++]; + var selOffsets = new int[numSel]; + for (int s = 0; s < numSel; s++) + { + selOffsets[s] = (short)GetWordValue(data, selPos); + selPos += 2; + int descLen = data[selPos++]; + selPos += descLen; + } + t.Command = TapeCommand.SELECT_BLOCK; + t.ControlOffsets = selOffsets; // add to tape _datacorder.DataBlocks.Add(t); @@ -1739,37 +1840,17 @@ This block will enable you to jump from one block to another within the file. Th All blocks are included in the block count!. */ - // not implemented properly - t = new TapeDataBlock(); t.BlockID = 0x23; t.DataPeriods = new List(); t.BlockDescription = BlockType.Jump_to_Block; - - int relativeJumpValue = GetWordValue(data, _position); - string result = string.Empty; - - switch (relativeJumpValue) - { - case 0: - result = "Loop Forever"; - break; - case 1: - result = "To Next Block"; - break; - case 2: - result = "Skip One Block"; - break; - case -1: - result = "Go to Previous Block"; - break; - } - - //t.BlockDescription = "[JUMP BLOCK - " + result +"]"; - t.PauseInMS = 0; t.PauseInTStates = 0; + // signed relative jump value, resolved during control-flow expansion + t.Command = TapeCommand.JUMP; + t.ControlValue = (short)GetWordValue(data, _position); + // add to tape _datacorder.DataBlocks.Add(t); @@ -1846,44 +1927,145 @@ the symbol and the number of times it must be repeated. Thus the length of the whole data stream in bits is NB*TOTD, or in bytes DS=ceil(NB*TOTD/8). ---- */ - // not currently implemented properly - - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x19; - t.BlockDescription = BlockType.Generalized_Data_Block; - t.DataPeriods = new List(); + t = new TapeDataBlock + { + BlockID = 0x19, + BlockDescription = BlockType.Generalized_Data_Block + }; int blockLen = GetInt32(data, _position); _position += 4; + int blockEnd = _position + blockLen; // the block body (from here) is blockLen bytes - int pause = GetWordValue(data, _position); + pauseLen = GetWordValue(data, _position); _position += 2; + t.PauseInMS = pauseLen; + t.PauseInTStates = TranslatePause(pauseLen); - int totp = GetInt32(data, _position); - _position += 4; - - int npp = data[_position++]; + int totp = GetInt32(data, _position); _position += 4; // symbols in the pilot/sync stream + int npp = data[_position++]; // max pulses per pilot/sync symbol + int asp = data[_position++]; if (asp == 0) asp = 256; // pilot/sync alphabet size - int asp = data[_position++]; + int totd = GetInt32(data, _position); _position += 4; // symbols in the data stream + int npd = data[_position++]; // max pulses per data symbol + int asd = data[_position++]; if (asd == 0) asd = 256; // data alphabet size - int totd = GetInt32(data, _position); - _position += 4; + // pilot and sync: a symbol-definition table followed by an RLE stream of (symbol, repetitions) + if (totp > 0) + { + ReadSymDefs(asp, npp, out var flags, out var pulses); + for (int i = 0; i < totp; i++) + { + int symbol = data[_position++]; + int reps = GetWordValue(data, _position); _position += 2; + if (symbol >= asp) continue; + for (int r = 0; r < reps; r++) + EmitSymbol(flags[symbol], pulses[symbol]); + } + } - int npd = data[_position++]; + // data: a symbol-definition table followed by a packed bitstream (NB bits per symbol, MSb first) + if (totd > 0) + { + ReadSymDefs(asd, npd, out var flags, out var pulses); + int nb = BitsPerSymbol(asd); + int streamStart = _position; + int bitPos = 0; + for (int s = 0; s < totd; s++) + { + int symbol = ReadBits(nb, streamStart, ref bitPos); + if (symbol < asd) + EmitSymbol(flags[symbol], pulses[symbol]); + } + } - int asd = data[_position++]; + // pause after the block + DoPause(); // add the block _datacorder.DataBlocks.Add(t); - // advance the position to the next block - _position += blockLen; + // advance to the next block (the packed bitstream may stop short of the declared block end) + _position = blockEnd; // ready for next block ZeroVars(); } + /// + /// Reads a Generalized Data Block symbol-definition table: 'count' symbols, each a flags byte followed + /// by 'maxp' pulse-length words. + /// + private void ReadSymDefs(int count, int maxp, out byte[] flags, out int[][] pulses) + { + flags = new byte[count]; + pulses = new int[count][]; + for (int i = 0; i < count; i++) + { + flags[i] = data[_position++]; + pulses[i] = new int[maxp]; + for (int p = 0; p < maxp; p++) + { + pulses[i][p] = GetWordValue(data, _position); + _position += 2; + } + } + } + + /// + /// Emits one Generalized Data Block symbol (a wave of pulses). The low 2 bits of the flags byte set the + /// starting polarity; a zero-length pulse terminates the wave early. + /// + private void EmitSymbol(byte flags, int[] pulses) + { + switch (flags & 0x03) + { + case 0: ToggleSignal(); break; // opposite to the current level (make an edge) - default + case 1: break; // same as the current level (prolong the previous pulse) + case 2: signal = false; break; // force low + case 3: signal = true; break; // force high + } + + for (int p = 0; p < pulses.Length; p++) + { + int len = pulses[p]; + if (len == 0) + break; // a zero-length pulse terminates the wave + if (p > 0) + ToggleSignal(); + t.DataPeriods.Add(len); + t.DataLevels.Add(signal); + } + } + + /// + /// Number of bits used to encode one data-stream symbol: ceil(log2(alphabetSize)). + /// + private static int BitsPerSymbol(int alphabetSize) + { + int nb = 0; + int v = alphabetSize - 1; + while (v > 0) { nb++; v >>= 1; } + return nb == 0 ? 1 : nb; + } + + /// + /// Reads 'nb' bits (MSb first) from the data stream beginning at 'streamStart', advancing 'bitPos'. + /// + private int ReadBits(int nb, int streamStart, ref int bitPos) + { + int val = 0; + for (int b = 0; b < nb; b++) + { + int byteIndex = streamStart + (bitPos >> 3); + int bit = byteIndex < data.Length ? (data[byteIndex] >> (7 - (bitPos & 7))) & 1 : 0; + val = (val << 1) | bit; + bitPos++; + } + return val; + } + // These mostly should be ignored by ZXHawk - here for completeness private void ProcessBlockID16() @@ -2086,12 +2268,28 @@ private void DecodeData() /// private void DoPause() { - if (pauseLen > 0) + // spec: a pause of zero duration is completely ignored and the current level does NOT change + if (pauseLen <= 0) + return; + + // spec: to finish the last edge there must be at least 1ms at the opposite level, after which the + // signal goes to 'low' for the remainder; a 'Pause' block always ends at the low level. + int oneMs = TranslatePause(1); + int total = t.PauseInTStates; + int edge = total < oneMs ? total : oneMs; + + ToggleSignal(); + t.DataPeriods.Add(edge); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Pause edge (opposite level): " + edge); + + int remainder = total - edge; + signal = false; // force the pause tail (and the level the next block starts from) to low + if (remainder > 0) { - t.DataPeriods.Add(t.PauseInTStates); - ToggleSignal(); + t.DataPeriods.Add(remainder); t.DataLevels.Add(signal); - t.PulseDescription.Add("Pause after block: " + t.PauseInTStates); + t.PulseDescription.Add("Pause silence (low): " + remainder); } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeCommand.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeCommand.cs deleted file mode 100644 index 28383dd8382..00000000000 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// Represents the possible commands that can be raised from each tape block - /// - public enum TapeCommand - { - NONE, - STOP_THE_TAPE, - STOP_THE_TAPE_48K, - BEGIN_GROUP, - END_GROUP, - SHOW_MESSAGE, - } -} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs index 35120b7f8da..c09ad7f2fdc 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs @@ -3,6 +3,7 @@ using System.Text; using BizHawk.Common.StringExtensions; +using BizHawk.Emulation.Cores.Tapes; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum { @@ -47,20 +48,13 @@ public WavConverter(DatacorderDevice _tapeDevice) /// public override bool CheckType(byte[] data) { - // WAV Header + // WAV Header - the "WAVE" identifier is at offset 8. Guard against shorter files: CheckType runs on + // every tape during type-detection, so a short file of another format must not throw here. + if (data == null || data.Length < 12) + return false; - // check whether this is a valid wav format file by looking at the identifier in the header string ident = Encoding.ASCII.GetString(data, 8, 4); - - if (!"WAVE".EqualsIgnoreCase(ident)) - { - // this is not a valid WAV format file - return false; - } - else - { - return true; - } + return "WAVE".EqualsIgnoreCase(ident); } /// diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.CpuLink.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.CpuLink.cs index 392b29f3f39..d90894f84eb 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.CpuLink.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.CpuLink.cs @@ -7,15 +7,15 @@ public partial class ZXSpectrum public readonly struct CpuLink(ZXSpectrum spectrum, SpectrumBase machine) : IZ80ALink { public byte FetchMemory(ushort address) - => machine.ReadMemory(address); + => machine.ReadMemoryMapped(address); public byte ReadMemory(ushort address) => spectrum._cdl == null - ? machine.ReadMemory(address) + ? machine.ReadMemoryMapped(address) : spectrum.ReadMemory_CDL(address); public void WriteMemory(ushort address, byte value) - => machine.WriteMemory(address, value); + => machine.WriteMemoryMapped(address, value); public byte ReadHardware(ushort address) => machine.ReadPort(address); diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs index c06b3c0d555..59b56228442 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs @@ -36,7 +36,35 @@ public bool FrameAdvance(IController controller, bool render, bool renderSound) _cpu.TraceCallback = null; } - _machine.ExecuteFrame(ren, renSound); + // Fast tape loading (loader acceleration): while a load is in progress - i.e. the tape has been + // auto-started - and instant loading is enabled on a non-deterministic core, run several whole + // machine-frames per host frame. The actual loader executes (just faster in wall-clock), so this + // covers every loading scheme (standard ROM, custom/turbo like Speedlock, CSW/WAV) and the loading + // screen and inter-block pauses appear naturally. FrameCount advances once per machine-frame, so it + // still matches an accurate load. Disabled when deterministic (TAS/movie recording). + int steps = 1; + if (!DeterministicEmulation + && SyncSettings.TapeLoadSpeed == TapeLoadSpeed.Instant + && _machine.TapeDevice is { TapeIsPlaying: true }) + { + // user-configurable machine-frames per host frame (clamped to the settings-slider range 5..50) + steps = SyncSettings.TapeLoadTurboMultiplier; + if (steps < 5) steps = 5; + else if (steps > 50) steps = 50; + } + + for (int i = 0; i < steps; i++) + { + bool last = i == steps - 1; + _machine.ExecuteFrame(ren && last, renSound && last); + + // discard the intermediate frames' audio so the mixer only returns the final frame's samples + // (otherwise the skipped frames' samples would pile up and the sound buffers would overflow) + if (!last) + { + SoundMixer.DiscardSamples(); + } + } if (_isLag) { @@ -61,6 +89,7 @@ public void ResetCounters() public void Dispose() { + _machine?.AYDevice?.Dispose(); _machine = null; } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs index e3964f319a5..da43834354b 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs @@ -36,6 +36,10 @@ public PutSettingsDirtyBits PutSettings(ZXSpectrumSettings o) { _machine.TapeBuzzer.Volume = o.TapeVolume; } + if (_machine?.ULADevice != null) + { + _machine.ULADevice.FrameBlend = o.GigascreenFrameBlend; + } Settings = o; @@ -87,6 +91,11 @@ public class ZXSpectrumSettings [DefaultValue(false)] public bool UseCoreBorderForBackground { get; set; } + [DisplayName("Gigascreen frame blending")] + [Description("Averages each frame with the previous one before display, mimicking CRT/TV persistence.\nMany Spectrum demos and games use 'gigascreen' - alternating two images every frame - to fake extra colours or resolution; without blending these flicker harshly, and with it they resolve into the intended smooth picture with soft shaded edges.\nDisplay-only: does not affect emulation, savestates or movies.")] + [DefaultValue(true)] + public bool GigascreenFrameBlend { get; set; } + public ZXSpectrumSettings Clone() { return (ZXSpectrumSettings)MemberwiseClone(); @@ -121,6 +130,11 @@ public class ZXSpectrumSyncSettings [DefaultValue(TapeLoadSpeed.Accurate)] public TapeLoadSpeed TapeLoadSpeed { get; set; } + [DisplayName("Turbo Load Multiplier")] + [Description("When Turbo Loading is enabled, how many machine-frames to run per host frame while a tape is loading. Higher = faster loading but lower on-screen framerate during the load")] + [DefaultValue(20)] + public int TapeLoadTurboMultiplier { get; set; } + [DisplayName("Joystick 1")] [Description("The emulated joystick assigned to P1 (SHOULD BE UNIQUE TYPE!)")] [DefaultValue(JoystickType.Kempston)] @@ -141,6 +155,11 @@ public class ZXSpectrumSyncSettings [DefaultValue(true)] public bool AutoLoadTape { get; set; } + [DisplayName("Pentagon Disk Drive")] + [Description("The physical Beta 128 floppy drive fitted to the Pentagon. Both drive types spin at 300 RPM (so the index and data timing that software depends on are identical); this only affects the secondary spin-up and track-to-track/settle timing. Only applies to the Pentagon 128 model.")] + [DefaultValue(PentagonDiskDriveType.Drive35Inch)] + public PentagonDiskDriveType PentagonDriveType { get; set; } + public ZXSpectrumSyncSettings Clone() { @@ -158,6 +177,22 @@ public static bool NeedsReboot(ZXSpectrumSyncSettings x, ZXSpectrumSyncSettings } } + /// + /// The physical floppy drive fitted to the Pentagon's Beta 128 interface + /// + public enum PentagonDiskDriveType + { + /// + /// 3.5" double-density drive (400 ms spin-up, 3 ms track-to-track) + /// + Drive35Inch, + + /// + /// 5.25" double-density drive (750 ms spin-up, 6 ms track-to-track) + /// + Drive525Inch, + } + /// /// Verbosity of the ZXHawk generated OSD messages /// @@ -210,13 +245,20 @@ public enum BorderType /// /// The speed at which the tape is loaded - /// NOT IN USE YET /// public enum TapeLoadSpeed { + /// + /// Cycle-accurate: the tape's pulses are replayed in real time. + /// Accurate, - //Fast, - //Fastest + + /// + /// Instant/flash load: the ROM load routine is trapped and the block's bytes are injected directly, + /// skipping pulse replay. Non-cycle-accurate, so it only takes effect when Deterministic Emulation + /// is off (a movie forces determinism, which disables it). + /// + Instant, } } @@ -319,15 +361,16 @@ public static ZXMachineMetaData GetMetaObject(MachineType type) m.Media = "3\" Floppy Disk (via built-in Floppy Drive)"; break; case MachineType.Pentagon128: - m.Name = "(NOT WORKING YET) Pentagon 128 Clone"; - m.Description = " "; - m.Description += " "; - m.Released = " "; - m.CPU = " "; - m.Memory = " "; - m.Video = " "; - m.Audio = " "; - m.Media = " "; + m.Name = "Pentagon 128 (Clone)"; + m.Description = "A Russian-built clone of the ZX Spectrum 128, constructed entirely from discrete 74-series TTL logic rather than a Ferranti ULA. "; + m.Description += "The discrete design gives it no memory contention (the CPU is never stalled by video RAM access) and a clean, slightly longer video frame - qualities that made it the favourite machine of the ex-USSR demoscene. "; + m.Description += "It integrates a clone of the Beta 128 disk interface running TR-DOS directly on the motherboard, so it boots from disk as well as tape, and it became the de-facto standard Spectrum across the former Soviet Union."; + m.Released = "1989 (first USSR clones)"; + m.CPU = "Zilog Z80A (clone) @ 3.5MHz"; + m.Memory = "48KB ROM (128 + 48 BASIC + TR-DOS) / 128KB RAM"; + m.Video = "Discrete TTL video @ 7MHz - PAL (50.00Hz Interrupt, 71680 T-States/frame, no memory contention)"; + m.Audio = "Beeper (HW 1ch. / 10oct.) & General Instruments AY-3-8912 PSG (3ch)"; + m.Media = "Cassette Tape & 3.5\"/5.25\" Floppy Disk (via built-in Beta 128 interface / TR-DOS)"; break; } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Messaging.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Messaging.cs index 65753de300b..4f281471ff5 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Messaging.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Messaging.cs @@ -81,6 +81,7 @@ public void OSD_DiskInserted() } sb.Append("DISK INSERTED (" + _machine.DiskMediaIndex + ": " + _diskInfo[_machine.DiskMediaIndex].Name + ")"); + sb.Append(" - Protection: " + _machine.UPDDiskDevice.ProtectionName); SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); } @@ -107,7 +108,7 @@ public void OSD_ShowDiskStatus() if (_machine.UPDDiskDevice != null) { - if (_machine.UPDDiskDevice.DiskPointer == null) + if (!_machine.UPDDiskDevice.DiskInserted) { sb.Append("No Disk Loaded"); SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); @@ -118,12 +119,7 @@ public void OSD_ShowDiskStatus() SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); sb.Clear(); - string protection = "None"; - protection = Enum.GetName(typeof(ProtectionType), _machine.UPDDiskDevice.DiskPointer.Protection); - if (protection == "None") - protection += " (OR UNKNOWN)"; - - sb.Append("Detected Protection: " + protection); + sb.Append("Detected Protection: " + _machine.UPDDiskDevice.ProtectionName); SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); sb.Clear(); @@ -204,6 +200,8 @@ public void OSD_TapeInserted() StringBuilder sb = new StringBuilder(); sb.Append("TAPE INSERTED (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + if (_machine.TapeDevice != null) + sb.Append(" - Loader: " + _machine.TapeDevice.DetectedLoaderName); SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); } @@ -377,6 +375,10 @@ public void OSD_ShowTapeStatus() SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); sb.Clear(); + sb.Append("Detected Loader: " + _machine.TapeDevice.DetectedLoaderName); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + sb.Clear(); + sb.Append("Block: "); sb.Append("(" + (_machine.TapeDevice.CurrentDataBlockIndex + 1) + " of " + _machine.TapeDevice.DataBlocks.Count + ") " + diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs index 1932abe480c..d4c886dece9 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs @@ -4,7 +4,7 @@ using BizHawk.Common; using BizHawk.Emulation.Common; -using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Components.Z80AOpt; using BizHawk.Emulation.Cores.Properties; using BizHawk.Emulation.Cores.Components; @@ -45,7 +45,7 @@ public ZXSpectrum( _gameInfo = lp.Roms.Select(r => r.Game).ToList(); - _cpu = new Z80A(default); + _cpu = new Z80AOpt(default); _tracer = new TraceBuffer(_cpu.TraceHeader); _files = lp.Roms.Select(r => r.RomData).ToList(); @@ -146,17 +146,31 @@ public ZXSpectrum( _machine.TapeBuzzer.Volume = settings.TapeVolume; } + if (_machine.ULADevice != null) + { + _machine.ULADevice.FrameBlend = settings.GigascreenFrameBlend; + } + DCFilter dc = new DCFilter(SoundMixer, 512); ser.Register(dc); ser.Register(new StateSerializer(SyncState)); HardReset(); SetupMemoryDomains(); + + // announce the inserted disk and its detected protection (media is loaded during machine + // construction, before _machine is assigned, so this can't be done from LoadAllMedia) + if (_machine.diskImages?.Count > 0) + OSD_DiskInserted(); + + // likewise announce the inserted tape and its detected loading scheme + if (_tapeInfo.Count > 0) + OSD_TapeInserted(); } public Action HardReset; public Action SoftReset; - private readonly Z80A _cpu; + private readonly Z80AOpt _cpu; private readonly TraceBuffer _tracer; public IController _controller; diff --git a/src/BizHawk.Emulation.Cores/Floppy/Controllers/IFdcHost.cs b/src/BizHawk.Emulation.Cores/Floppy/Controllers/IFdcHost.cs new file mode 100644 index 00000000000..b515c5ffa3b --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Controllers/IFdcHost.cs @@ -0,0 +1,23 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// The seam a machine core (ZX Spectrum +3, Amstrad CPC, ...) implements to wire the uPD765 controller + /// into its bus. The controller calls these when its output lines change; a host is free to ignore a line + /// its hardware leaves unconnected. On the +3 and CPC the interrupt and DMA lines are not wired to the + /// Z80 (software polls the main status register), so those cores can implement these as no-ops. + /// + public interface IFdcHost + { + /// + /// The FDC interrupt request (INT) line changed. Raised at result-phase start and on seek + /// completion; lowered when the result is read or the seek interrupt is sensed. + /// + void OnFdcInterrupt(bool asserted); + + /// + /// The FDC data request (DRQ) line changed. Only meaningful to DMA-driven hosts; the +3/CPC + /// transfer bytes by polling RQM instead and can ignore this. + /// + void OnFdcDataRequest(bool asserted); + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Controllers/Upd765Fdc.cs b/src/BizHawk.Emulation.Cores/Floppy/Controllers/Upd765Fdc.cs new file mode 100644 index 00000000000..fe80adb8dea --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Controllers/Upd765Fdc.cs @@ -0,0 +1,816 @@ +using System.Collections.Generic; + +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// NEC uPD765 floppy disk controller operating on the flux disk model. Implements the + /// command/execution/result phase machine and the main-status-register handshake, reading sectors off + /// the drive's MFM flux and computing ST0-ST3 (CRC errors, deleted marks, wrong cylinder, no data) as + /// the real device does, rather than replaying image status bytes. + /// This increment adds real timing: the host advances the controller with Clock, seeks step at the + /// programmed step rate, execution-phase bytes appear at the data-rate cadence (32 us per DD byte) with + /// overrun detection, and the interrupt line is driven through IFdcHost. Implemented commands: Specify, + /// Recalibrate, Seek, Sense Interrupt Status, Sense Drive Status, Read ID and Read Data. Write/Format/ + /// Scan are later increments. The command opcode is masked with 0x1F (five bits) so Scan/Version decode + /// correctly. + /// + public sealed class Upd765Fdc + { + // Main status register bits + private const byte MSR_D0B = 0x01, MSR_CB = 0x10, MSR_EXM = 0x20, MSR_DIO = 0x40, MSR_RQM = 0x80; + // ST0 + private const byte ST0_HD = 0x04, ST0_NR = 0x08, ST0_EC = 0x10, ST0_SE = 0x20, ST0_IC_ABTERM = 0x40, ST0_IC_INVALID = 0x80; + // ST1 + private const byte ST1_MA = 0x01, ST1_NW = 0x02, ST1_ND = 0x04, ST1_OR = 0x10, ST1_DE = 0x20, ST1_EN = 0x80; + // ST2 + private const byte ST2_MD = 0x01, ST2_BC = 0x02, ST2_WC = 0x10, ST2_DD = 0x20, ST2_CM = 0x40; + // ST3 + private const byte ST3_TS = 0x08, ST3_T0 = 0x10, ST3_RY = 0x20, ST3_WP = 0x40, ST3_FT = 0x80; + + private enum Phase { Idle, Command, Execution, Result } + + // opcodes (low 5 bits of the command byte) + private const int CmdReadData = 0x06, CmdReadId = 0x0A, CmdSpecify = 0x03, CmdSeek = 0x0F, + CmdRecalibrate = 0x07, CmdSenseInt = 0x08, CmdSenseDrive = 0x04, + CmdWriteData = 0x05, CmdWriteDeleted = 0x09, CmdFormat = 0x0D, + CmdReadDeleted = 0x0C, CmdVersion = 0x10, CmdReadDiagnostic = 0x02, + CmdScanEqual = 0x11, CmdScanLow = 0x19, CmdScanHigh = 0x1D; + + /// + /// Up to four drives; the +3/CPC populate index 0. Host sets these. + /// + public IFloppyDrive[] Drives { get; } = new IFloppyDrive[4]; + + /// + /// Optional host callback for the INT/DRQ lines. Null is fine (a polling host ignores them). + /// + public IFdcHost Host { get; set; } + + /// + /// RNG for weak/fuzzy sectors, shared so repeated reads vary; seedable for determinism. + /// + public WeakBitRng WeakRng { get; set; } = new WeakBitRng(0); + + /// + /// Host CPU clock the timing is expressed against; defaults to the +3 Z80 clock. + /// + public long CpuClockHz { get; private set; } = 3_546_900; + + /// + /// Current state of the FDC interrupt request line. + /// + public bool IntPending => _intActive; + + /// + /// True while a command is in progress (not idle) - drives the disk activity light. + /// + public bool Active => _phase != Phase.Idle; + + private Phase _phase = Phase.Idle; + + private int _opcode; + private bool _mt, _mf, _sk; + private byte[] _params = new byte[9]; + private int _paramCount, _paramNeeded; + + private byte[] _result = new byte[7]; + private int _resPtr, _resLen; + + private byte[] _exec = System.Array.Empty(); + private int _execPtr; + private bool _execByteReady; // a transfer byte is available (delivered/accepted on demand) + private bool _execWrite; // execution direction: true = host writes into the FDC (Write/Format) + private bool _formatMode; // the current write execution is a Format Track (ID bytes, not data) + + // write / format bookkeeping (target sectors decoded from the current track, modified then rebuilt) + private List _writeList; + private readonly List _writeTargets = new List(); + private bool _writeDeleted; + private byte _formatFiller; + + // Specify parameters (millisecond scale); only the step rate feeds a derived threshold now + private int _srtMs = 6, _hltMs = 2; + private bool _nonDma; + private int _cyclesPerStep; + + // per-drive overlapped-seek bookkeeping + private readonly bool[] _seekActive = new bool[4]; + private readonly bool[] _seekSettling = new bool[4]; + private readonly int[] _seekTarget = new int[4]; + private readonly int[] _seekStepTimer = new int[4]; + private readonly int[] _seekSettleTimer = new int[4]; + private readonly bool[] _seekIntPending = new bool[4]; + private readonly byte[] _pcn = new byte[4]; + + // per-drive rotating Read ID position + private readonly int[] _readIdIndex = new int[4]; + + private int _unit; + private int _side; + private bool _intActive; + + private IFloppyDrive ActiveDrive => Drives[_unit & 3]; + + public Upd765Fdc() => RecomputeTiming(); + + /// + /// Point the controller (and its drives) at the host CPU clock so timing lands correctly. + /// + public void ConfigureTiming(long cpuClockHz) + { + CpuClockHz = cpuClockHz > 0 ? cpuClockHz : 3_546_900; + RecomputeTiming(); + for (int i = 0; i < 4; i++) Drives[i]?.ConfigureTiming(CpuClockHz); + } + + /// + /// Serialize the controller's operational state (the loaded disk is restored separately). + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("Upd765Fdc"); + ser.SyncEnum(nameof(_phase), ref _phase); + ser.Sync(nameof(_opcode), ref _opcode); + ser.Sync(nameof(_mt), ref _mt); ser.Sync(nameof(_mf), ref _mf); ser.Sync(nameof(_sk), ref _sk); + ser.Sync(nameof(_params), ref _params, useNull: false); + ser.Sync(nameof(_paramCount), ref _paramCount); + ser.Sync(nameof(_paramNeeded), ref _paramNeeded); + ser.Sync(nameof(_result), ref _result, useNull: false); + ser.Sync(nameof(_resPtr), ref _resPtr); + ser.Sync(nameof(_resLen), ref _resLen); + ser.Sync(nameof(_exec), ref _exec, useNull: false); + ser.Sync(nameof(_execPtr), ref _execPtr); + ser.Sync(nameof(_execByteReady), ref _execByteReady); + ser.Sync(nameof(_execWrite), ref _execWrite); + ser.Sync(nameof(_formatMode), ref _formatMode); + ser.Sync(nameof(_srtMs), ref _srtMs); + ser.Sync(nameof(_hltMs), ref _hltMs); + ser.Sync(nameof(_nonDma), ref _nonDma); + for (int i = 0; i < 4; i++) + { + ser.Sync("_seekActive" + i, ref _seekActive[i]); + ser.Sync("_seekSettling" + i, ref _seekSettling[i]); + ser.Sync("_seekTarget" + i, ref _seekTarget[i]); + ser.Sync("_seekStepTimer" + i, ref _seekStepTimer[i]); + ser.Sync("_seekSettleTimer" + i, ref _seekSettleTimer[i]); + ser.Sync("_seekIntPending" + i, ref _seekIntPending[i]); + ser.Sync("_pcn" + i, ref _pcn[i]); + ser.Sync("_readIdIndex" + i, ref _readIdIndex[i]); + } + ser.Sync(nameof(_unit), ref _unit); + ser.Sync(nameof(_side), ref _side); + ser.Sync(nameof(_intActive), ref _intActive); + // weak-cell RNG state, so weak-sector reads replay identically across savestate/TAS load + ulong weakState = WeakRng.State; + ser.Sync("_weakRngState", ref weakState); + WeakRng.State = weakState; + ser.EndSection(); + RecomputeTiming(); + } + + private void RecomputeTiming() + { + _cyclesPerStep = (int)(CpuClockHz * _srtMs / 1000); // seek step-rate time (Specify SRT) + if (_cyclesPerStep < 1) _cyclesPerStep = 1; + } + + public void Reset() + { + _phase = Phase.Idle; + _paramCount = _resPtr = _resLen = _execPtr = 0; + _execByteReady = _execWrite = _formatMode = false; + _writeTargets.Clear(); + _writeList = null; + for (int i = 0; i < 4; i++) + { + _seekActive[i] = _seekSettling[i] = _seekIntPending[i] = false; + _seekTarget[i] = _seekStepTimer[i] = _seekSettleTimer[i] = _readIdIndex[i] = 0; + _pcn[i] = 0; + } + LowerInt(); + } + + // ---- clock tick: advances drives and overlapped seeks ---- + // + // Note: execution-phase data transfer is NOT clocked to a byte cadence. The +3 has no DMA and no + // wired FDC interrupt - it transfers each byte by polling RQM in a tight loop - so the host itself + // paces the transfer. Emulating a fixed byte cadence on top of lazy clock-on-access races against + // that poll loop (a byte can appear "overrun" between the status poll and the data read), which + // breaks real loaders. Bytes are therefore delivered/accepted on demand, with no overrun. The + // data-based nature of copy protection (weak bits, geometry) is preserved by the flux model itself; + // only the transfer-rate timing is dropped, exactly as the original hardware-agnostic core did. + + public void Clock(int cpuCycles) + { + for (int i = 0; i < 4; i++) Drives[i]?.Clock(cpuCycles); + ClockSeeks(cpuCycles); + } + + private void ClockSeeks(int cpuCycles) + { + for (int u = 0; u < 4; u++) + { + if (!_seekActive[u]) continue; + + if (_seekSettling[u]) + { + // head is settling after the final step; complete once the settle time elapses + _seekSettleTimer[u] += cpuCycles; + if (_seekSettleTimer[u] >= SettleCycles(u)) CompleteSeek(u); + continue; + } + + _seekStepTimer[u] += cpuCycles; + int stepCycles = EffectiveStepCycles(u); + while (_seekActive[u] && !_seekSettling[u] && _seekStepTimer[u] >= stepCycles) + { + _seekStepTimer[u] -= stepCycles; + var drive = Drives[u]; + int cur = drive?.CurrentCylinder ?? _seekTarget[u]; + if (cur != _seekTarget[u]) + { + drive?.Step(cur < _seekTarget[u]); + _pcn[u] = (byte)(drive?.CurrentCylinder ?? _seekTarget[u]); + } + if ((drive?.CurrentCylinder ?? _seekTarget[u]) == _seekTarget[u]) + { + if (SettleCycles(u) <= 0) + { + CompleteSeek(u); + } + else + { + // begin settling, carrying any cycles left over past the final step + _seekSettling[u] = true; + _seekSettleTimer[u] = _seekStepTimer[u]; + _seekStepTimer[u] = 0; + if (_seekSettleTimer[u] >= SettleCycles(u)) CompleteSeek(u); + } + } + } + } + } + + private void CompleteSeek(int u) + { + _seekActive[u] = false; + _seekSettling[u] = false; + _seekIntPending[u] = true; + RaiseInt(); + } + + // Step interval bounded below by the drive's mechanical track-to-track time (the programmed step + // rate cannot make the head move faster than the mechanism). + private int EffectiveStepCycles(int u) + { + int cycles = _cyclesPerStep; + int t2t = Drives[u]?.TrackToTrackMs ?? 0; + if (t2t > 0) + { + int floor = (int)(CpuClockHz * t2t / 1000); + if (floor > cycles) cycles = floor; + } + return cycles < 1 ? 1 : cycles; + } + + private int SettleCycles(int u) + { + int settleMs = Drives[u]?.SettleMs ?? 0; + return settleMs <= 0 ? 0 : (int)(CpuClockHz * settleMs / 1000); + } + + // ---- host-facing register interface (the host maps I/O ports to these) ---- + + public byte ReadStatus() + { + byte s = 0; + switch (_phase) + { + case Phase.Idle: s = MSR_RQM; break; + case Phase.Command: s = MSR_RQM | MSR_CB; break; + case Phase.Execution: + s = MSR_CB | MSR_EXM; + if (!_execWrite) s |= MSR_DIO; // DIO=1 read (FDC->CPU), 0 write (CPU->FDC) + if (_execByteReady) s |= MSR_RQM; + break; + case Phase.Result: s = MSR_RQM | MSR_CB | MSR_DIO; break; + } + for (int i = 0; i < 4; i++) if (_seekActive[i]) s |= (byte)(MSR_D0B << i); + return s; + } + + public byte ReadData() + { + if (_phase == Phase.Execution) + { + if (_execWrite || !_execByteReady) return 0xFF; // wrong direction + byte b = _exec[_execPtr++]; + if (_execPtr >= _exec.Length) EnterResult(); // last byte consumed (clears the ready flag) + return b; // on demand: the next byte is immediately available while bytes remain + } + if (_phase == Phase.Result) + { + if (_resPtr == 0) LowerInt(); // reading the result acknowledges the interrupt + byte b = _result[_resPtr++]; + if (_resPtr >= _resLen) _phase = Phase.Idle; + return b; + } + return 0xFF; + } + + public void WriteData(byte data) + { + switch (_phase) + { + case Phase.Idle: + BeginCommand(data); + break; + case Phase.Command: + _params[_paramCount++] = data; + if (_paramCount >= _paramNeeded) Execute(); + break; + case Phase.Execution: + if (!_execWrite || !_execByteReady) return; // wrong direction + _exec[_execPtr++] = data; + if (_execPtr >= _exec.Length) FinalizeWriteExec(); // last byte accepted (clears the ready flag) + break; + } + } + + // ---- command handling ---- + + private void BeginCommand(byte cmd) + { + _mt = (cmd & 0x80) != 0; + _mf = (cmd & 0x40) != 0; + _sk = (cmd & 0x20) != 0; + _opcode = cmd & 0x1F; // five-bit opcode (fixes Scan/Version vs the old 0x0F mask) + _paramCount = 0; + _resPtr = 0; + + _paramNeeded = _opcode switch + { + CmdReadData => 8, + CmdReadDeleted => 8, + CmdReadDiagnostic => 8, + CmdScanEqual => 8, + CmdScanLow => 8, + CmdScanHigh => 8, + CmdWriteData => 8, + CmdWriteDeleted => 8, + CmdFormat => 5, + CmdReadId => 1, + CmdSpecify => 2, + CmdSeek => 2, + CmdRecalibrate => 1, + CmdSenseInt => 0, + CmdSenseDrive => 1, + CmdVersion => 0, + _ => 0, + }; + + _phase = Phase.Command; + if (_paramNeeded == 0) Execute(); + } + + private void Execute() + { + switch (_opcode) + { + case CmdSpecify: DoSpecify(); break; + case CmdRecalibrate: DoSeek(recalibrate: true); break; + case CmdSeek: DoSeek(recalibrate: false); break; + case CmdSenseInt: DoSenseInterrupt(); break; + case CmdSenseDrive: DoSenseDrive(); break; + case CmdReadId: DoReadId(); break; + case CmdReadData: DoReadData(readDeleted: false); break; + case CmdReadDeleted: DoReadData(readDeleted: true); break; + case CmdWriteData: DoWriteData(deleted: false); break; + case CmdWriteDeleted: DoWriteData(deleted: true); break; + case CmdFormat: DoFormat(); break; + case CmdVersion: DoVersion(); break; + default: DoInvalid(); break; + } + } + + private void DoSpecify() + { + _srtMs = 16 - (_params[0] >> 4); + _hltMs = _params[1] & 0xFE; + _nonDma = (_params[1] & 0x01) != 0; + if (_srtMs < 1) _srtMs = 1; + RecomputeTiming(); + _phase = Phase.Idle; // no result + } + + private void DoSeek(bool recalibrate) + { + _unit = _params[0] & 3; + int target = recalibrate ? 0 : _params[1]; + _seekTarget[_unit] = target; + _seekStepTimer[_unit] = 0; + _seekSettleTimer[_unit] = 0; + _seekActive[_unit] = true; + _seekSettling[_unit] = false; + _seekIntPending[_unit] = false; + _phase = Phase.Idle; // command accepted; completion is timed and reported via Sense Interrupt + } + + private void DoSenseInterrupt() + { + int unit = -1; + for (int i = 0; i < 4; i++) if (_seekIntPending[i]) { unit = i; break; } + + if (unit < 0) + { + // no interrupt pending + _result[0] = ST0_IC_INVALID; + _resLen = 1; + } + else + { + _seekIntPending[unit] = false; + byte st0 = (byte)(ST0_SE | (unit & 3)); + var drive = Drives[unit]; + if (drive == null || !drive.Ready) st0 |= ST0_NR; + _result[0] = st0; + _result[1] = _pcn[unit]; + _resLen = 2; + } + EnterResultPrepared(); + } + + private void DoSenseDrive() + { + _unit = _params[0] & 3; + _side = (_params[0] >> 2) & 1; + var drive = ActiveDrive; + + byte st3 = (byte)(_unit & 3); + if (_side != 0) st3 |= ST0_HD; + if (drive == null) + { + st3 |= ST3_FT; // no drive present + } + else + { + if (drive.WriteProtected) st3 |= ST3_WP; + if (drive.Track0) st3 |= ST3_T0; + if (drive.Ready) st3 |= ST3_RY; + if (drive.SideCount > 1) st3 |= ST3_TS; + } + _result[0] = st3; + _resLen = 1; + EnterResultPrepared(); + } + + private void DoReadId() + { + _unit = _params[0] & 3; + _side = (_params[0] >> 2) & 1; + var drive = ActiveDrive; + byte st0 = (byte)((_side << 2) | (_unit & 3)); + + if (drive == null || !drive.Ready) + { + // genuinely not ready (no disk / motor off) -> ST0 Not Ready + st0 |= ST0_IC_ABTERM | ST0_NR; + PrepareResultChrn(st0, 0, 0, 0, 0, 0, 0); + EnterResultPrepared(); + return; + } + var track = drive.CurrentTrack(_side); + var sectors = track == null ? null : StandardMfmFormat.DecodeSectors(track, WeakRng); + if (sectors == null || sectors.Count == 0) + { + // drive IS ready but the track has no readable address mark -> Missing Address Mark, NOT Not Ready + // (a real uPD765 only sets NR when the drive itself is not ready; the frontend maps NR to + // "disk not ready", so a track with damaged/absent marks must report MA, not NR). + st0 |= ST0_IC_ABTERM; + PrepareResultChrn(st0, ST1_MA, 0, 0, 0, 0, 0); + EnterResultPrepared(); + return; + } + + int idx = _readIdIndex[_unit] % sectors.Count; + _readIdIndex[_unit] = (idx + 1) % sectors.Count; + var s = sectors[idx]; + byte st1 = s.IdCrcOk ? (byte)0 : ST1_DE; + PrepareResultChrn(st0, st1, 0, s.C, s.H, s.R, s.N); + EnterResultPrepared(); + } + + // Read Data (readDeleted=false) and Read Deleted Data (readDeleted=true). They differ only in which + // data-address-mark type is "expected": a mismatch (a deleted sector under Read Data, or a normal + // sector under Read Deleted Data) sets the Control Mark (ST2 CM) bit - and is skipped when SK is set, + // otherwise it is transferred and terminates the command. RoboCop and similar titles store protected + // data under deleted address marks and read it with Read Deleted Data (0x0C). + private void DoReadData(bool readDeleted) + { + _unit = _params[0] & 3; + _side = (_params[0] >> 2) & 1; + byte cyl = _params[1], head = _params[2], sector = _params[3], n = _params[4], eot = _params[5]; + var drive = ActiveDrive; + byte st0 = (byte)((_side << 2) | (_unit & 3)); + + if (drive == null || !drive.Ready) + { + st0 |= ST0_IC_ABTERM | ST0_NR; + PrepareResultChrn(st0, 0, 0, cyl, head, sector, n); + EnterResultPrepared(); + return; + } + var track = drive.CurrentTrack(_side); + if (track == null) + { + // drive ready but the current cylinder has no track (unformatted) -> Missing Address Mark, not NR + st0 |= ST0_IC_ABTERM; + PrepareResultChrn(st0, ST1_MA, 0, cyl, head, sector, n); + EnterResultPrepared(); + return; + } + + var sectors = StandardMfmFormat.DecodeSectors(track, WeakRng); + var data = new List(); + byte st1 = 0, st2 = 0; + byte curR = sector; + + for (; ; ) + { + var s = FindById(sectors, cyl, head, curR, n); + if (s == null) + { + // requested sector not found on the track + st1 |= ST1_ND; + st0 |= ST0_IC_ABTERM; + break; + } + + bool markMismatch = s.Deleted != readDeleted; // wrong address-mark type for this command + if (markMismatch && _sk) + { + // SK set: skip a wrong-mark sector without transferring it + if (curR == eot) { st1 |= ST1_EN; break; } + curR++; + continue; + } + + if (!s.IdCrcOk) st1 |= ST1_DE; + if (!s.DataCrcOk) { st1 |= ST1_DE; st2 |= ST2_DD; } + if (markMismatch) st2 |= ST2_CM; + + data.AddRange(s.Data); + + bool crcError = (st1 & ST1_DE) != 0; // DE is set for both ID- and data-field CRC failures + if (curR == eot || crcError || markMismatch) + { + st1 |= ST1_EN; + break; + } + curR++; + } + + PrepareResultChrn(st0, st1, st2, cyl, head, curR, n); + if (data.Count > 0) + { + _exec = data.ToArray(); + _execPtr = 0; + _execByteReady = true; // first byte available on demand + _phase = Phase.Execution; + } + else + { + EnterResultPrepared(); + } + } + + // Version: the +3 uses a uPD765A, which reports 0x80. + private void DoVersion() + { + _result[0] = 0x80; + _resLen = 1; + EnterResultPrepared(); + } + + private void DoWriteData(bool deleted) + { + _unit = _params[0] & 3; + _side = (_params[0] >> 2) & 1; + byte cyl = _params[1], head = _params[2], sector = _params[3], n = _params[4], eot = _params[5]; + var drive = ActiveDrive; + byte st0 = (byte)((_side << 2) | (_unit & 3)); + + if (drive == null || !drive.Ready) + { + st0 |= ST0_IC_ABTERM | ST0_NR; + PrepareResultChrn(st0, 0, 0, cyl, head, sector, n); + EnterResultPrepared(); + return; + } + var track = drive.CurrentTrack(_side); + if (track == null) + { + // drive ready but no track at this cylinder -> no address mark to write into, not NR + st0 |= ST0_IC_ABTERM; + PrepareResultChrn(st0, ST1_MA, 0, cyl, head, sector, n); + EnterResultPrepared(); + return; + } + if (drive.WriteProtected) + { + st0 |= ST0_IC_ABTERM; + PrepareResultChrn(st0, ST1_NW, 0, cyl, head, sector, n); + EnterResultPrepared(); + return; + } + + // decode the current track, then locate the ID of each sector to be written (R..EOT) + _writeList = StandardMfmFormat.ToTrackSectors(track, WeakRng); + _writeTargets.Clear(); + int expected = 0; + byte curR = sector; + for (; ; ) + { + int idx = FindSectorIndex(_writeList, cyl, head, curR, n); + if (idx < 0) + { + st0 |= ST0_IC_ABTERM; + PrepareResultChrn(st0, ST1_ND, 0, cyl, head, curR, n); + EnterResultPrepared(); + return; + } + _writeTargets.Add(idx); + expected += _writeList[idx].SizeBytes; + if (curR == eot) break; + curR++; + } + + _writeDeleted = deleted; + BeginWriteExecution(expected, format: false); + PrepareResultChrn(st0, ST1_EN, deleted ? ST2_CM : (byte)0, cyl, head, curR, n); + } + + private void DoFormat() + { + _unit = _params[0] & 3; + _side = (_params[0] >> 2) & 1; + byte n = _params[1], sc = _params[2]; + _formatFiller = _params[4]; + var drive = ActiveDrive; + byte st0 = (byte)((_side << 2) | (_unit & 3)); + + if (drive == null || !drive.Ready) + { + st0 |= ST0_IC_ABTERM | ST0_NR; + PrepareResultChrn(st0, 0, 0, 0, 0, 0, n); + EnterResultPrepared(); + return; + } + if (drive.WriteProtected) + { + st0 |= ST0_IC_ABTERM; + PrepareResultChrn(st0, ST1_NW, 0, 0, 0, 0, n); + EnterResultPrepared(); + return; + } + if (sc == 0) + { + PrepareResultChrn(st0, 0, 0, 0, 0, 0, n); + EnterResultPrepared(); + return; + } + + // the host supplies four ID bytes (C H R N) per sector during execution + BeginWriteExecution(sc * 4, format: true); + PrepareResultChrn(st0, 0, 0, 0, 0, 0, n); + } + + private void BeginWriteExecution(int expectedBytes, bool format) + { + _exec = new byte[expectedBytes]; + _execPtr = 0; + _execByteReady = true; // ready to accept the first byte on demand + _execWrite = true; + _formatMode = format; + _phase = Phase.Execution; + } + + private void FinalizeWriteExec() + { + if (_formatMode) FinalizeFormat(); else FinalizeWrite(); + } + + private void FinalizeWrite() + { + int offset = 0; + foreach (int idx in _writeTargets) + { + var ts = _writeList[idx]; + int size = ts.SizeBytes; + var buf = new byte[size]; + System.Array.Copy(_exec, offset, buf, 0, System.Math.Min(size, _exec.Length - offset)); + ts.Data = buf; + ts.Deleted = _writeDeleted; + ts.DataCrcError = false; // a fresh write lays down a correct CRC + ts.WeakCopies = null; // writing over a weak sector makes it deterministic + offset += size; + } + ActiveDrive?.WriteTrack(_side, StandardMfmFormat.BuildStandardTrack(_writeList)); + EnterResult(); // result prepared with EN at command start + } + + private void FinalizeFormat() + { + int sc = _exec.Length / 4; + var list = new List(sc); + byte lastC = 0, lastH = 0, lastR = 0, lastN = 0; + for (int i = 0; i < sc; i++) + { + byte c = _exec[i * 4], h = _exec[i * 4 + 1], r = _exec[i * 4 + 2], n = _exec[i * 4 + 3]; + int size = 128 << (n & 7); + var data = new byte[size]; + for (int b = 0; b < size; b++) data[b] = _formatFiller; + list.Add(new TrackSector { C = c, H = h, R = r, N = n, Data = data }); + lastC = c; lastH = h; lastR = r; lastN = n; + } + ActiveDrive?.WriteTrack(_side, StandardMfmFormat.BuildStandardTrack(list)); + _result[3] = lastC; _result[4] = lastH; _result[5] = lastR; _result[6] = lastN; + EnterResult(); + } + + private void DoInvalid() + { + _result[0] = ST0_IC_INVALID; + _resLen = 1; + EnterResultPrepared(); + } + + // ---- helpers ---- + + private static DecodedSector FindById(List sectors, byte c, byte h, byte r, byte n) + { + foreach (var s in sectors) + if (s.R == r && s.C == c && s.H == h && s.N == n) + return s; + return null; + } + + private static int FindSectorIndex(List sectors, byte c, byte h, byte r, byte n) + { + for (int i = 0; i < sectors.Count; i++) + { + var s = sectors[i]; + if (s.R == r && s.C == c && s.H == h && s.N == n) return i; + } + return -1; + } + + // Fill the standard 7-byte result (ST0 ST1 ST2 C H R N) without changing phase yet. + private void PrepareResultChrn(byte st0, byte st1, byte st2, byte c, byte h, byte r, byte n) + { + _result[0] = st0; _result[1] = st1; _result[2] = st2; + _result[3] = c; _result[4] = h; _result[5] = r; _result[6] = n; + _resLen = 7; + } + + // Enter result phase after the execution buffer is exhausted (result already prepared). + private void EnterResult() + { + _execByteReady = _execWrite = _formatMode = false; + _resPtr = 0; + _phase = Phase.Result; + RaiseInt(); + } + + // Enter result phase directly for commands with no execution streaming. + private void EnterResultPrepared() + { + _resPtr = 0; + if (_resLen > 0) + { + _phase = Phase.Result; + RaiseInt(); + } + else + { + _phase = Phase.Idle; + } + } + + private void RaiseInt() + { + if (_intActive) return; + _intActive = true; + Host?.OnFdcInterrupt(true); + } + + private void LowerInt() + { + if (!_intActive) return; + _intActive = false; + Host?.OnFdcInterrupt(false); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Controllers/Wd1793Fdc.cs b/src/BizHawk.Emulation.Cores/Floppy/Controllers/Wd1793Fdc.cs new file mode 100644 index 00000000000..e6e3527d960 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Controllers/Wd1793Fdc.cs @@ -0,0 +1,814 @@ +using System.Collections.Generic; + +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Western Digital WD1793 (and its Soviet KR1818VG93 clone) floppy disk controller, operating on the + /// shared flux disk model. This is the FDC at the heart of the Beta 128 disk interface built into the + /// Pentagon and Scorpion ZX Spectrum clones, running TR-DOS. It mirrors the uPD765 core in this folder: + /// it drives the same IFloppyDrive array, reads sectors off the drive's MFM flux with + /// StandardMfmFormat.DecodeSectors (computing CRC/record-not-found as the real device does rather than + /// replaying image status), advances on a Clock tick against the host CPU clock, and drives its + /// interrupt (INTRQ) and data-request (DRQ) lines through IFdcHost. + /// The WD1793 is register-based rather than the uPD765's command/parameter/result phases. Five registers + /// are exposed (Status/Command share an address, then Track, Sector, Data); the Beta interface maps them + /// to Z80 ports 0x1F/0x3F/0x5F/0x7F and adds an external system latch (drive select, side, density, + /// reset) plus a status read of INTRQ/DRQ. Those live on the Beta device wrapper, not here. + /// Commands are grouped into four types with distinct status-register meanings: + /// Type I - Restore, Seek, Step, Step-In, Step-Out (head positioning). + /// Type II - Read Sector, Write Sector. + /// Type III - Read Address, Read Track, Write Track. + /// Type IV - Force Interrupt. + /// + public sealed class Wd1793Fdc + { + // Status register - Type I (positioning) commands + private const byte ST_BUSY = 0x01; // shared bit 0: command in progress + private const byte ST1_INDEX = 0x02; // index pulse under the head + private const byte ST1_TRACK0 = 0x04; // head positioned at track 0 + private const byte ST1_CRC = 0x08; // CRC error (in an ID field during verify) + private const byte ST1_SEEKERR = 0x10; // target track not verified + private const byte ST1_HEADLOADED = 0x20; + private const byte ST_WRITEPROT = 0x40; // shared bit 6 (write protect) + private const byte ST_NOTREADY = 0x80; // shared bit 7 (drive not ready) + + // Status register - Type II/III (data-transfer) commands + private const byte ST2_DRQ = 0x02; // data register needs service + private const byte ST2_LOSTDATA = 0x04; // host missed a DRQ + private const byte ST2_CRC = 0x08; // CRC error in ID or data field + private const byte ST2_RNF = 0x10; // record not found + private const byte ST2_RECTYPE = 0x20; // (read) deleted DAM / (write) write fault + // bit 6 write protect, bit 7 not ready shared with above + + /// + /// Internal execution state of the current command. + /// + private enum State + { + Idle, + TypeISeek, // issuing step pulses toward the target cylinder + TypeISettle, // head settling delay after the final step + TypeIVerify, // verifying the ID field track matches the track register + ReadSearch, // locating the requested sector on the current track + ReadTransfer, // streaming the found sector's bytes to the data register + WriteWait, // waiting for the host to supply the first data byte + WriteTransfer, // accepting sector bytes from the host + ReadAddress, // streaming the 6-byte ID field + RnfWait, // sector/ID not found - spinning out the search before flagging Record Not Found + } + + /// + /// Up to four drives; the Beta interface can address A-D. Host populates these. + /// + public IFloppyDrive[] Drives { get; } = new IFloppyDrive[4]; + + /// + /// Optional host callback for the INTRQ/DRQ lines. The Beta reads them from its system port, so a + /// polling host can leave this null and read IntRequest / DataRequest directly instead. + /// + public IFdcHost Host { get; set; } + + /// + /// RNG for weak/fuzzy sectors, shared so repeated reads vary; seedable for determinism. + /// + public WeakBitRng WeakRng { get; set; } = new WeakBitRng(0); + + /// + /// Host CPU clock the timing is expressed against; defaults to the Pentagon Z80 clock. + /// + public long CpuClockHz { get; private set; } = 3_546_900; + + /// + /// The INTRQ line: set when a command completes, cleared when the status register is read or a new + /// command is written. The Beta surfaces this on system-port read bit 7. + /// + public bool IntRequest => _intrq; + + /// + /// The DRQ line: set when the data register needs servicing during a Type II/III transfer. The Beta + /// surfaces this on system-port read bit 6. + /// + public bool DataRequest => _drq; + + /// + /// True while a command is in progress - drives the disk activity light. + /// + public bool Active => _state != State.Idle; + + // registers + private byte _status; + private byte _command; + private byte _trackReg; + private byte _sectorReg; + private byte _dataReg; + + // external latch state, set by the Beta system port + private int _unit; // selected drive 0-3 + private int _side; // selected physical side 0/1 + private bool _dden = true; // double density (MFM); FM is unused by TR-DOS + + private State _state = State.Idle; + private bool _intrq; + private bool _drq; + private bool _typeIStatus = true; // whether _status is composed as Type I (positioning) or Type II/III + + // positioning bookkeeping + private int _seekTargetCyl; + private int _stepDir; // +1 toward higher cylinders, -1 toward track 0 + private bool _verifyAfterSeek; + private int _cyclesPerStep; // from the command's r1r0 step-rate field + private int _settleCycles; // Type II/III E-flag head settle (15 ms) + private int _rnfTimeout; // how long a data-command searches for its ID before Record Not Found + // note: no per-byte transfer cadence field - reads and writes are handed over on demand (see ReadData) + private int _verifySettleCycles; // Type I verify head settle (30 ms at the Beta's 1 MHz FDC clock) + private int _timer; // counts down the current timed sub-step + + // data-transfer bookkeeping + private byte[] _xfer = System.Array.Empty(); + private int _xferPtr; + private bool _xferCrcError; // the located sector failed its data CRC + private bool _multi; // Type II multiple-record flag + private bool _deletedData; // read: located a deleted DAM / write: lay a deleted DAM + + // write / format bookkeeping (the current track decoded to an editable form, modified then rebuilt) + private bool _formatMode; // the active write transfer is a Write Track (format), not a sector + private List _writeSectors; + private int _writeIndex; // index into _writeSectors of the Write Sector target + private readonly List _formatBuf = new List(8192); + private int _readAddrIndex; // rotating index so successive Read Address commands step sectors + + private IFloppyDrive ActiveDrive => Drives[_unit & 3]; + + private static readonly int[] StepRatesMs = { 6, 12, 20, 30 }; // r1r0 at the Beta's 1 MHz FDC clock + + public Wd1793Fdc() => RecomputeTiming(); + + /// + /// Point the controller (and its drives) at the host CPU clock so timing lands correctly. + /// + public void ConfigureTiming(long cpuClockHz) + { + CpuClockHz = cpuClockHz > 0 ? cpuClockHz : 3_546_900; + RecomputeTiming(); + for (int i = 0; i < 4; i++) Drives[i]?.ConfigureTiming(CpuClockHz); + } + + private void RecomputeTiming() + { + // Head settle. Per the FD179X datasheet the Type II/III E-flag delay is 15 ms; the Type I verify + // settle is also 15 ms at a 2 MHz clock but DOUBLES to 30 ms at a 1 MHz clock - and the Beta clocks + // the WD1793 at 1 MHz for double-density 5.25"/3.5" drives, so the verify settle is 30 ms here. + _settleCycles = (int)(CpuClockHz * 15 / 1000); + _verifySettleCycles = (int)(CpuClockHz * 30 / 1000); + // the datasheet gives Record Not Found after ~5 unsuccessful revolutions; at 300 RPM that is + // ~1 s (5 x 200 ms) = CpuClockHz cycles. Present sectors are found immediately (we decode the whole + // track), so this only delays the RNF report for a genuinely absent ID - the "disk error" pause. + _rnfTimeout = (int)CpuClockHz; + if (_rnfTimeout < 1) _rnfTimeout = 1; + } + + /// + /// Serialize the controller's operational state (the loaded disk is restored separately). + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("Wd1793Fdc"); + ser.Sync(nameof(_status), ref _status); + ser.Sync(nameof(_command), ref _command); + ser.Sync(nameof(_trackReg), ref _trackReg); + ser.Sync(nameof(_sectorReg), ref _sectorReg); + ser.Sync(nameof(_dataReg), ref _dataReg); + ser.Sync(nameof(_unit), ref _unit); + ser.Sync(nameof(_side), ref _side); + ser.Sync(nameof(_dden), ref _dden); + ser.SyncEnum(nameof(_state), ref _state); + ser.Sync(nameof(_intrq), ref _intrq); + ser.Sync(nameof(_drq), ref _drq); + ser.Sync(nameof(_typeIStatus), ref _typeIStatus); + ser.Sync(nameof(_seekTargetCyl), ref _seekTargetCyl); + ser.Sync(nameof(_stepDir), ref _stepDir); + ser.Sync(nameof(_verifyAfterSeek), ref _verifyAfterSeek); + ser.Sync(nameof(_cyclesPerStep), ref _cyclesPerStep); + ser.Sync(nameof(_timer), ref _timer); + ser.Sync(nameof(_xfer), ref _xfer, useNull: false); + ser.Sync(nameof(_xferPtr), ref _xferPtr); + ser.Sync(nameof(_xferCrcError), ref _xferCrcError); + ser.Sync(nameof(_multi), ref _multi); + ser.Sync(nameof(_deletedData), ref _deletedData); + ser.Sync(nameof(_formatMode), ref _formatMode); + ser.Sync(nameof(_writeIndex), ref _writeIndex); + ser.Sync(nameof(_readAddrIndex), ref _readAddrIndex); + ulong weakState = WeakRng.State; + ser.Sync("_weakRngState", ref weakState); + WeakRng.State = weakState; + ser.EndSection(); + RecomputeTiming(); + } + + public void Reset() + { + _status = 0; + _command = 0; + _trackReg = 0; + _sectorReg = 0; + _dataReg = 0; + _state = State.Idle; + _drq = false; + _typeIStatus = true; + _xfer = System.Array.Empty(); + _xferPtr = 0; + RaiseIntrq(false); + // a hardware reset also issues a Restore (seek to track 0); model it lazily by parking heads + for (int i = 0; i < 4; i++) Drives[i]?.SeekTo(0); + _trackReg = 0; + } + + // ---- Beta system-latch inputs (called by the Beta device on a #FF write) ---- + + /// + /// Select the active drive (0-3) and physical side (0/1), and the density (true = double / MFM). + /// The Beta system latch drives these lines externally to the WD1793. + /// + public void SetSystem(int drive, int side, bool doubleDensity) + { + _unit = drive & 3; + _side = side & 1; + _dden = doubleDensity; + if (ActiveDrive != null) ActiveDrive.MotorOn = true; + } + + // ---- register access (mapped by the Beta to ports 0x1F/0x3F/0x5F/0x7F) ---- + + /// + /// Read the status register (Beta port 0x1F). Reading it clears the INTRQ line. + /// + public byte ReadStatus() + { + RaiseIntrq(false); + return ComposeStatus(); + } + + /// + /// Write the command register (Beta port 0x1F). Decodes the command type and starts execution. + /// + public void WriteCommand(byte value) + { + _command = value; + // a Force Interrupt (Type IV) is accepted even while busy; every other command is ignored if busy + int top = value >> 4; + if (top == 0xD) { ForceInterrupt(value); return; } + if ((_status & ST_BUSY) != 0) return; + + RaiseIntrq(false); + if (value < 0x80) BeginTypeI(value); + else if (value < 0xC0) BeginTypeII(value); + else BeginTypeIII(value); + } + + /// + /// Read the track register (Beta port 0x3F). + /// + public byte ReadTrack() => _trackReg; + + /// + /// Write the track register (Beta port 0x3F). + /// + public void WriteTrack(byte value) => _trackReg = value; + + /// + /// Read the sector register (Beta port 0x5F). + /// + public byte ReadSector() => _sectorReg; + + /// + /// Write the sector register (Beta port 0x5F). + /// + public void WriteSector(byte value) => _sectorReg = value; + + /// + /// Read the data register (Beta port 0x7F). Bytes are handed over on demand: the current byte is + /// returned and the next is loaded, DRQ staying asserted until the sector is exhausted. This matches + /// how the +3's uPD765 controller transfers - the FDC is advanced lazily between the host's polls, so + /// pacing bytes to a fixed cadence would race the poll loop and spuriously trip Lost Data; TR-DOS + /// polls and never falls behind, and the flux model still enforces the data-based side of protection. + /// + public byte ReadData() + { + byte v = _dataReg; + if ((_state == State.ReadTransfer || _state == State.ReadAddress) && _drq) + { + _xferPtr++; + if (_xferPtr < _xfer.Length) + { + _dataReg = _xfer[_xferPtr]; + } + else + { + _drq = false; + RaiseDrq(false); + if (_state == State.ReadAddress) FinishReadAddress(); + else FinishReadSector(); + } + } + return v; + } + + /// + /// Write the data register (Beta port 0x7F). Bytes are accepted on demand (same rationale as ReadData): + /// a Write Sector fills the target sector's data field; a Write Track accumulates the format byte stream + /// until a track's worth has been supplied. DRQ stays asserted until the transfer is complete. + /// + public void WriteData(byte value) + { + _dataReg = value; + if (_state != State.WriteTransfer || !_drq) return; + + if (_formatMode) + { + _formatBuf.Add(value); + // one DD track is ~6250 bytes at 250 kbit/s x 300 RPM; that is the index-to-index write window + if (_formatBuf.Count >= 6250) + { + _drq = false; + RaiseDrq(false); + FinishWriteTrack(); + } + return; + } + + _xfer[_xferPtr++] = value; + if (_xferPtr >= _xfer.Length) + { + _drq = false; + RaiseDrq(false); + FinishWriteSector(); + } + } + + // ---- command decode ---- + + private void BeginTypeI(byte cmd) + { + _typeIStatus = true; + _status = ST_BUSY; + RaiseIntrq(false); + + _cyclesPerStep = (int)(CpuClockHz * StepRatesMs[cmd & 0x03] / 1000); + if (_cyclesPerStep < 1) _cyclesPerStep = 1; + _verifyAfterSeek = (cmd & 0x04) != 0; // V flag + + int type = cmd >> 4; + var drive = ActiveDrive; + int cur = drive?.CurrentCylinder ?? 0; + + switch (type) + { + case 0x0: // Restore: seek track 0 + _seekTargetCyl = 0; + _trackReg = (byte)cur; + break; + case 0x1: // Seek: to the cylinder in the data register + _seekTargetCyl = _dataReg; + break; + case 0x2: // Step (repeat last direction) - default toward higher + case 0x3: + _seekTargetCyl = cur + (_stepDir >= 0 ? 1 : -1); + break; + case 0x4: // Step-In (toward higher cylinders) + case 0x5: + _stepDir = +1; + _seekTargetCyl = cur + 1; + break; + case 0x6: // Step-Out (toward track 0) + case 0x7: + _stepDir = -1; + _seekTargetCyl = cur - 1; + break; + } + + if (_seekTargetCyl < 0) _seekTargetCyl = 0; + _stepDir = _seekTargetCyl >= cur ? +1 : -1; + _timer = _cyclesPerStep; + _state = State.TypeISeek; + } + + private void BeginTypeII(byte cmd) + { + _typeIStatus = false; + _status = ST_BUSY; + _multi = (cmd & 0x10) != 0; + bool write = (cmd & 0x20) != 0; + _deletedData = write && (cmd & 0x01) != 0; + _timer = (cmd & 0x04) != 0 ? _settleCycles : 0; // E flag = 15 ms settle before access + + if (write) StartWriteSector(); // sets WriteTransfer (or errors out) + else _state = State.ReadSearch; + } + + private void BeginTypeIII(byte cmd) + { + _typeIStatus = false; + _status = ST_BUSY; + _multi = false; + _deletedData = false; + _timer = (cmd & 0x04) != 0 ? _settleCycles : 0; + + int type = cmd >> 4; + switch (type) + { + case 0xC: StartReadAddress(); break; + case 0xE: StartReadTrack(); break; // TODO: raw track read (verbatim gap/mark/data stream) + case 0xF: StartWriteTrack(); break; // TODO: track format + } + } + + private void ForceInterrupt(byte cmd) + { + // abort any command; the low nibble selects the interrupt condition (immediate = bit 3). + _state = State.Idle; + _status &= unchecked((byte)~ST_BUSY); + _drq = false; + RaiseDrq(false); + _typeIStatus = true; + if ((cmd & 0x08) != 0) RaiseIntrq(true); // immediate interrupt + } + + // ---- clock tick ---- + + public void Clock(int cpuCycles) + { + for (int i = 0; i < 4; i++) Drives[i]?.Clock(cpuCycles); + + switch (_state) + { + case State.Idle: + break; + + case State.TypeISeek: + TickSeek(cpuCycles); + break; + + case State.TypeISettle: + if ((_timer -= cpuCycles) <= 0) + { + if (_verifyAfterSeek) { _state = State.TypeIVerify; } + else FinishTypeI(); + } + break; + + case State.TypeIVerify: + DoVerify(); + break; + + case State.ReadSearch: + if ((_timer -= cpuCycles) <= 0) DoReadSearch(); + break; + + case State.RnfWait: + if ((_timer -= cpuCycles) <= 0) FinishTypeIIError(ST2_RNF); + break; + + case State.ReadTransfer: + case State.ReadAddress: + // bytes are handed over on demand as the data register is read, not on a clock cadence + break; + + case State.WriteWait: + case State.WriteTransfer: + // bytes are accepted on demand as the data register is written, not on a clock cadence + break; + } + } + + private void TickSeek(int cpuCycles) + { + var drive = ActiveDrive; + if ((_timer -= cpuCycles) > 0) return; + + int cur = drive?.CurrentCylinder ?? 0; + if (cur == _seekTargetCyl) + { + // arrived: on a verify, take the 30 ms head-settle (1 MHz clock) before checking the ID + if (_verifyAfterSeek && _verifySettleCycles > 0) { _timer = _verifySettleCycles; _state = State.TypeISettle; } + else if (_verifyAfterSeek) { _state = State.TypeIVerify; } + else FinishTypeI(); + return; + } + + bool up = _seekTargetCyl > cur; + drive?.Step(up); + _trackReg = (byte)(drive?.CurrentCylinder ?? 0); + _timer = _cyclesPerStep; + } + + private void DoVerify() + { + var drive = ActiveDrive; + var track = drive?.CurrentTrack(_side); + bool ok = false; + if (track != null) + { + foreach (var s in StandardMfmFormat.DecodeSectors(track, WeakRng)) + { + if (!s.IdCrcOk) continue; + if (s.C == _trackReg) { ok = true; break; } + } + } + if (!ok) _status |= ST1_SEEKERR; + FinishTypeI(); + } + + private void FinishTypeI() + { + _status &= unchecked((byte)~ST_BUSY); + _state = State.Idle; + RaiseIntrq(true); + } + + // ---- Type II: Read Sector ---- + + private void DoReadSearch() + { + var drive = ActiveDrive; + if (drive == null || !drive.Ready) + { + FinishTypeIIError(ST_NOTREADY); + return; + } + + var track = drive.CurrentTrack(_side); + var sectors = track == null ? null : StandardMfmFormat.DecodeSectors(track, WeakRng); + DecodedSector found = null; + if (sectors != null) + { + foreach (var s in sectors) + { + // match the sector-register R (and the track-register C); side compare is left to the + // Beta side latch, which already selected the physical side above. + if (s.R == _sectorReg && s.C == _trackReg) { found = s; break; } + } + } + + if (found == null) + { + BeginRnfWait(); + return; + } + + _xfer = found.Data ?? System.Array.Empty(); + _xferPtr = 0; + _xferCrcError = !found.DataCrcOk; + _deletedData = found.Deleted; + if (_xfer.Length == 0) { FinishReadSector(); return; } + // on-demand transfer: first byte ready now, DRQ held until the sector is drained by ReadData + _dataReg = _xfer[0]; + _drq = true; + RaiseDrq(true); + _state = State.ReadTransfer; + } + + private void FinishReadSector() + { + if (_xferCrcError) _status |= ST2_CRC; + if (_deletedData) _status |= ST2_RECTYPE; + if (_multi) + { + // multiple-record: advance to the next sector and search again on the next clock + _sectorReg++; + _state = State.ReadSearch; + _timer = 0; + return; + } + _status &= unchecked((byte)~ST_BUSY); + _state = State.Idle; + RaiseIntrq(true); + } + + private void FinishReadAddress() + { + if (_xferCrcError) _status |= ST2_CRC; + _status &= unchecked((byte)~ST_BUSY); + _state = State.Idle; + RaiseIntrq(true); + } + + private void FinishTypeIIError(byte statusBit) + { + _status = statusBit; + _status &= unchecked((byte)~ST_BUSY); + _state = State.Idle; + RaiseIntrq(true); + } + + // The requested ID was not found on the (ready) drive. The real device keeps searching for ~5 + // revolutions before flagging Record Not Found; hold BUSY for that long so software sees the + // characteristic delay rather than an instant error. + private void BeginRnfWait() + { + _timer = _rnfTimeout; + _state = State.RnfWait; + } + + // ---- Type III: Read Address (returns the 6-byte ID field of the next sector) ---- + + private void StartReadAddress() + { + var drive = ActiveDrive; + if (drive == null || !drive.Ready) { FinishTypeIIError(ST_NOTREADY); return; } + + var track = drive.CurrentTrack(_side); + var sectors = track == null ? null : StandardMfmFormat.DecodeSectors(track, WeakRng); + if (sectors == null || sectors.Count == 0) { BeginRnfWait(); return; } + + // rotate through the track's IDs on successive calls, approximating the sector that happens to be + // under the head as the disk turns (TR-DOS reads addresses repeatedly to map a track) + var s = sectors[_readAddrIndex % sectors.Count]; + _readAddrIndex++; + // the 6 ID bytes: track, side, sector, length, then the ID-field CRC (big-endian, as on disk) + ushort idCrc = StandardMfmFormat.IdFieldCrc(s.C, s.H, s.R, s.N); + _xfer = new byte[] { s.C, s.H, s.R, s.N, (byte)(idCrc >> 8), (byte)(idCrc & 0xFF) }; + _xferPtr = 0; + _xferCrcError = !s.IdCrcOk; + // Read Address copies the track byte of the ID into the sector register (per the datasheet) + _sectorReg = s.C; + _dataReg = _xfer[0]; + _drq = true; + RaiseDrq(true); + _state = State.ReadAddress; + } + + // ---- Type II Write Sector / Type III Read Track, Write Track: next increment ---- + + private void StartWriteSector() + { + var drive = ActiveDrive; + if (drive == null || !drive.Ready) { FinishTypeIIError(ST_NOTREADY); return; } + if (drive.WriteProtected) { FinishTypeIIError(ST_WRITEPROT); return; } + + // decode the current track to an editable form and locate the target sector + _writeSectors = StandardMfmFormat.ToTrackSectors(drive.CurrentTrack(_side), WeakRng); + _writeIndex = -1; + for (int i = 0; i < _writeSectors.Count; i++) + if (_writeSectors[i].R == _sectorReg && _writeSectors[i].C == _trackReg) { _writeIndex = i; break; } + if (_writeIndex < 0) { BeginRnfWait(); return; } + + // accept the sector's data bytes on demand, then rebuild the flux track (see WriteData/FinishWriteSector) + _xfer = new byte[_writeSectors[_writeIndex].SizeBytes]; + _xferPtr = 0; + _formatMode = false; + _drq = true; + RaiseDrq(true); + _state = State.WriteTransfer; + } + + private void FinishWriteSector() + { + var ts = _writeSectors[_writeIndex]; + ts.Data = _xfer; + ts.Deleted = _deletedData; + ts.DataCrcError = false; // a fresh write lays down a correct CRC + _writeSectors[_writeIndex] = ts; + ActiveDrive?.WriteTrack(_side, StandardMfmFormat.BuildStandardTrack(_writeSectors)); + + if (_multi) + { + _sectorReg++; + StartWriteSector(); // next record (re-decodes the just-written track) + return; + } + _status &= unchecked((byte)~ST_BUSY); + _state = State.Idle; + RaiseIntrq(true); + } + + private void StartReadTrack() + { + var drive = ActiveDrive; + if (drive == null || !drive.Ready) { FinishTypeIIError(ST_NOTREADY); return; } + var sectors = StandardMfmFormat.DecodeSectors(drive.CurrentTrack(_side), WeakRng); + if (sectors == null || sectors.Count == 0) { BeginRnfWait(); return; } + + // synthesize a representative IBM System-34 byte stream for the whole track (gaps, sync, address + // marks, ID + data fields). This is a clean reconstruction from the decoded sectors rather than a + // verbatim cell-level dump; enough for TR-DOS/copiers that scan the track, CRC bytes left as 0. + var buf = new List(sectors.Count * 400); + foreach (var s in sectors) + { + for (int g = 0; g < 12; g++) buf.Add(0x4E); + buf.Add(0x00); buf.Add(0x00); buf.Add(0x00); + buf.Add(0xA1); buf.Add(0xA1); buf.Add(0xA1); + buf.Add(0xFE); buf.Add(s.C); buf.Add(s.H); buf.Add(s.R); buf.Add(s.N); + buf.Add(0x00); buf.Add(0x00); // ID CRC placeholder + for (int g = 0; g < 22; g++) buf.Add(0x4E); + buf.Add(0x00); buf.Add(0x00); buf.Add(0x00); + buf.Add(0xA1); buf.Add(0xA1); buf.Add(0xA1); + buf.Add(s.Deleted ? (byte)0xF8 : (byte)0xFB); + if (s.Data != null) buf.AddRange(s.Data); + buf.Add(0x00); buf.Add(0x00); // data CRC placeholder + } + + _xfer = buf.ToArray(); + _xferPtr = 0; + _xferCrcError = false; + _deletedData = false; + _multi = false; + _dataReg = _xfer[0]; + _drq = true; + RaiseDrq(true); + _state = State.ReadTransfer; + } + + private void StartWriteTrack() + { + var drive = ActiveDrive; + if (drive == null || !drive.Ready) { FinishTypeIIError(ST_NOTREADY); return; } + if (drive.WriteProtected) { FinishTypeIIError(ST_WRITEPROT); return; } + + // accept the host's format byte stream on demand (see WriteData); FinishWriteTrack parses it + _formatBuf.Clear(); + _formatMode = true; + _drq = true; + RaiseDrq(true); + _state = State.WriteTransfer; + } + + private void FinishWriteTrack() + { + var sectors = ParseFormatStream(_formatBuf); + if (sectors.Count > 0) + ActiveDrive?.WriteTrack(_side, StandardMfmFormat.BuildStandardTrack(sectors)); + _status &= unchecked((byte)~ST_BUSY); + _state = State.Idle; + RaiseIntrq(true); + } + + // Parse a WD179X Write Track (format) byte stream into sectors. In the stream the host writes gap + // filler (0x4E), sync (0x00), then 0xF5 bytes that the FDC turns into A1 sync marks, an 0xFE ID + // address mark followed by C H R N and 0xF7 (which writes the ID CRC), more gap/sync, an 0xFB (or + // 0xF8 for a deleted) data address mark, the data bytes, and 0xF7 (data CRC). We scan for the address + // marks and read the fixed-size data field after each. + private static List ParseFormatStream(List buf) + { + var list = new List(); + int i = 0, n = buf.Count; + while (i < n) + { + if (buf[i++] != 0xFE) continue; // seek the next ID address mark + if (i + 4 > n) break; + byte c = buf[i++], h = buf[i++], r = buf[i++], nn = buf[i++]; + var ts = new TrackSector { C = c, H = h, R = r, N = nn }; + // advance to the data address mark (skip the ID CRC and the gap between ID and data) + while (i < n && buf[i] != 0xFB && buf[i] != 0xF8 && buf[i] != 0xFE) i++; + int size = 128 << (nn & 7); + var data = new byte[size]; + if (i < n && (buf[i] == 0xFB || buf[i] == 0xF8)) + { + ts.Deleted = buf[i] == 0xF8; + i++; + for (int k = 0; k < size && i < n; k++) data[k] = buf[i++]; + } + ts.Data = data; + list.Add(ts); + } + return list; + } + + // ---- status composition + line drivers ---- + + private byte ComposeStatus() + { + var drive = ActiveDrive; + byte s = _status; + + // bit 7 not ready is meaningful for every command + if (drive == null || !drive.Ready) s |= ST_NOTREADY; else s &= unchecked((byte)~ST_NOTREADY); + + if (_typeIStatus) + { + if (drive != null && drive.Track0) s |= ST1_TRACK0; else s &= unchecked((byte)~ST1_TRACK0); + if (drive != null && drive.Index) s |= ST1_INDEX; else s &= unchecked((byte)~ST1_INDEX); + if (drive != null && drive.MotorOn) s |= ST1_HEADLOADED; + if (drive != null && drive.WriteProtected) s |= ST_WRITEPROT; + } + else + { + if (_drq) s |= ST2_DRQ; else s &= unchecked((byte)~ST2_DRQ); + } + return s; + } + + private void RaiseIntrq(bool asserted) + { + if (_intrq == asserted) return; + _intrq = asserted; + Host?.OnFdcInterrupt(asserted); + } + + private void RaiseDrq(bool asserted) + { + Host?.OnFdcDataRequest(asserted); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/CpcDskConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/CpcDskConverter.cs new file mode 100644 index 00000000000..b5ecf01bf28 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/CpcDskConverter.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Reader for the CPCEMU DSK and Extended DSK (EDSK) disk-image formats (used by +3 and CPC + /// software), producing per-track sector lists that synthesize into MFM flux tracks. Written from the + /// documented on-disk layout, not ported from any existing implementation. + /// Standard DSK stores uniform 128*2^N sector data. EDSK adds a per-track size table and per-sector + /// actual data lengths, which is how weak sectors are recorded: a data length that is an exact multiple + /// of the sector size means several copies are stored, and the byte positions that disagree across + /// copies are the weak (fuzzy) bits, mapped here to weak cells in the flux. + /// + public static class CpcDskConverter + { + public sealed class ParsedTrack + { + public int Cylinder; + public int Side; + public byte Gap3; + public readonly List Sectors = new List(); + + public MfmTrack BuildFlux() => StandardMfmFormat.BuildStandardTrack(Sectors, Gap3 == 0 ? 78 : Gap3); + } + + public sealed class ParsedDisk + { + public bool Extended; + public int TrackCount; + public int SideCount; + public readonly List Tracks = new List(); + } + + /// + /// A plain DSK dump of a Speedlock-protected title carries no weak-sector data, so the protection + /// check (which reads the weak sector several times expecting differing bytes and a data CRC error) + /// would fail. When Speedlock is detected and the weak sector has no recorded copies, synthesize a + /// few differing copies over its weak byte range - BuildStandardTrack then flags those cells weak and + /// the FDC reads them unpredictably, reproducing the protection. Images that already carry weak data + /// (EDSK multi-copy) are left untouched. + /// + public static void ApplySpeedlockWeakSynthesis(ParsedDisk disk) + { + var t0 = disk?.Tracks.Find(t => t.Cylinder == 0 && t.Side == 0); + if (t0 == null || t0.Sectors.Count != 9) return; + if (!DiskProtection.IsSpeedlock(t0.Sectors.Count, t0.Sectors[0].Data)) return; + + var weak = t0.Sectors[1]; // Speedlock's weak sector is the second one (id 2) + // The genuine Speedlock weak sector always carries a data CRC error in the dump; synthesize only + // then. A plain deleted-DAM data sector (valid CRC) must NOT be corrupted - some titles that carry + // the Speedlock loader signature still store real data in sector 2. + if (weak.WeakCopies != null || !weak.DataCrcError) return; // already weak, or not the weak sector + + int size = weak.SizeBytes; + var copy0 = new byte[size]; + System.Array.Copy(weak.Data, 0, copy0, 0, System.Math.Min(size, weak.Data.Length)); + var copy1 = (byte[])copy0.Clone(); + var copy2 = (byte[])copy0.Clone(); + + var (offset, length) = DiskProtection.SpeedlockWeakRegion(copy0); + for (int i = offset; i < offset + length && i < size; i++) + { + copy1[i] = (byte)(copy0[i] ^ 0xFF); + copy2[i] = (byte)(copy0[i] + 0x55); + } + weak.WeakCopies = new[] { copy0, copy1, copy2 }; + } + + private const string StdIdent = "MV - CPC"; + private const string ExtIdent = "EXTENDED"; + private const string TrackIdent = "Track-Info"; + + public static bool IsCpcDsk(byte[] d) + => d != null && d.Length >= 0x100 && (Match(d, 0, StdIdent) || Match(d, 0, ExtIdent)); + + public static ParsedDisk Parse(byte[] d) + { + if (!IsCpcDsk(d)) throw new ArgumentException("not a CPC DSK/EDSK image", nameof(d)); + + var disk = new ParsedDisk + { + Extended = Match(d, 0, ExtIdent), + TrackCount = d[0x30], + SideCount = d[0x31] == 0 ? 1 : d[0x31], + }; + + int stdTrackSize = d[0x32] | (d[0x33] << 8); // standard DSK: one uniform track size + int entries = disk.TrackCount * disk.SideCount; + int offset = 0x100; // track data begins after the 256-byte disk header + + for (int i = 0; i < entries; i++) + { + int trackSize = disk.Extended ? d[0x34 + i] * 256 : stdTrackSize; + if (disk.Extended && trackSize == 0) + continue; // unformatted track: no Track-Info block, no data + + if (offset + 0x100 > d.Length) break; + if (!Match(d, offset, TrackIdent)) { offset += trackSize; continue; } + + var pt = new ParsedTrack + { + Cylinder = d[offset + 0x10], + // A single-sided image keeps all tracks on side 0. This matters for the +3 double-sided + // workflow: a DS disk is split into two single-sided images, but the split leaves the + // second image's track headers still marked side 1 - honouring that would hide the disk + // from the single-headed drive (which only reads side 0). + Side = disk.SideCount <= 1 ? 0 : d[offset + 0x11], + Gap3 = d[offset + 0x16], + }; + + int numSectors = d[offset + 0x15]; + int siPos = offset + 0x18; // sector information list (8 bytes each) + int dataPos = offset + 0x100; // sector data area + + for (int s = 0; s < numSectors; s++) + { + int p = siPos + s * 8; + if (p + 8 > d.Length) break; + + byte c = d[p], h = d[p + 1], r = d[p + 2], n = d[p + 3]; + byte st1 = d[p + 4], st2 = d[p + 5]; + int declared = 128 << (n & 7); + int actual = disk.Extended ? (d[p + 6] | (d[p + 7] << 8)) : declared; + if (actual == 0) actual = declared; + + int avail = Math.Max(0, Math.Min(actual, d.Length - dataPos)); + var raw = new byte[actual]; + if (avail > 0) Array.Copy(d, dataPos, raw, 0, avail); + dataPos += actual; + + var ts = new TrackSector + { + C = c, H = h, R = r, N = n, + // image status bytes -> synthesized flux conditions + Deleted = (st2 & 0x40) != 0, // ST2 CM: deleted data address mark + DataCrcError = (st2 & 0x20) != 0, // ST2 DD: data-field CRC error + IdCrcError = (st1 & 0x20) != 0 && (st2 & 0x20) == 0, // ST1 DE without DD: ID-field CRC error + }; + + if (actual > declared && declared > 0 && actual % declared == 0) + { + // several stored copies -> weak sector; keep them so differing bytes become weak cells + int copies = actual / declared; + var arr = new byte[copies][]; + for (int cpy = 0; cpy < copies; cpy++) + { + arr[cpy] = new byte[declared]; + Array.Copy(raw, cpy * declared, arr[cpy], 0, declared); + } + ts.WeakCopies = arr; + ts.Data = arr[0]; + } + else + { + var data = new byte[declared]; + Array.Copy(raw, 0, data, 0, Math.Min(declared, raw.Length)); + ts.Data = data; + } + + pt.Sectors.Add(ts); + } + + disk.Tracks.Add(pt); + offset += trackSize; + } + + return disk; + } + + private static bool Match(byte[] d, int at, string ascii) + { + if (at + ascii.Length > d.Length) return false; + for (int i = 0; i < ascii.Length; i++) + if (d[at + i] != (byte)ascii[i]) return false; + return true; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/FdiConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/FdiConverter.cs new file mode 100644 index 00000000000..b1d6c0b7509 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/FdiConverter.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Loader for the ZX Spectrum FDI ("Full Disk Image", UKV/Ramsoft) sector format. FDI is a + /// sector-level container (per-track sector lists with CHRN, a flags byte, and a data pointer); we + /// synthesize a clean IBM System-34 MFM track from each track's sectors. FDI is most often a TR-DOS / + /// Beta Disk image read by a WD1793 controller rather than the uPD765 - but this conversion is + /// controller-agnostic (it produces standard MFM flux), so the eventual WD1793 core would read the same + /// FluxDisk this produces. + /// + public static class FdiConverter + { + public static bool IsFdi(byte[] d) + => d != null && d.Length >= 14 && d[0] == (byte)'F' && d[1] == (byte)'D' && d[2] == (byte)'I'; + + public static FluxDisk ToFluxDisk(byte[] d) + { + if (!IsFdi(d)) throw new System.ArgumentException("not an FDI file (no FDI signature)", nameof(d)); + + bool writeProtected = d[0x03] != 0; + int cylinders = ReadLe16(d, 0x04); + int heads = ReadLe16(d, 0x06); + int dataArea = ReadLe16(d, 0x0A); + int extraLen = ReadLe16(d, 0x0C); + + var disk = new FluxDisk { WriteProtected = writeProtected }; + int th = 0x0E + extraLen; // first track header + + for (int i = 0; i < cylinders * heads; i++) + { + if (th + 7 > d.Length) break; + int trackOffset = ReadLe32(d, th); + int sectorCount = d[th + 6]; + int cyl = i / heads; + int head = i % heads; + + var secs = new List(sectorCount); + int sd = th + 7; // first sector descriptor + for (int s = 0; s < sectorCount; s++) + { + if (sd + 7 > d.Length) break; + byte c = d[sd], h = d[sd + 1], r = d[sd + 2], n = d[sd + 3], flags = d[sd + 4]; + int secOffset = ReadLe16(d, sd + 5); + sd += 7; + + int size = 128 << (n & 7); + var data = new byte[size]; + int abs = dataArea + trackOffset + secOffset; + int copy = System.Math.Min(size, System.Math.Max(0, d.Length - abs)); + if (abs >= 0 && copy > 0) System.Array.Copy(d, abs, data, 0, copy); + + secs.Add(new TrackSector + { + C = c, H = h, R = r, N = n, Data = data, + Deleted = (flags & 0x80) != 0, // bit 7 = deleted data address mark + DataCrcError = (flags & 0x3F) == 0, // no CRC-valid bits set => the sector had a CRC error + }); + } + + disk.SetTrack(cyl, head, StandardMfmFormat.BuildStandardTrack(secs)); + th = sd; // next track header follows the last sector descriptor + } + return disk; + } + + private static int ReadLe16(byte[] d, int o) => d[o] | (d[o + 1] << 8); + private static int ReadLe32(byte[] d, int o) => d[o] | (d[o + 1] << 8) | (d[o + 2] << 16) | (d[o + 3] << 24); + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/HfeConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/HfeConverter.cs new file mode 100644 index 00000000000..6275e9daf35 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/HfeConverter.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// One track/side extracted from an HFE image: the raw MFM cell bitstream as an MfmTrack. + /// + public sealed class HfeTrack + { + public int Cylinder; + public int Side; + public MfmTrack Track; + } + + /// + /// Loader for the HxC Floppy Emulator HFE v1/v2 format ("HXCPICFE"). HFE stores each track as + /// a raw cell bitstream (clock + data), LSB first - which is exactly how MfmTrack packs its cells - so a + /// track loads by de-interleaving the two sides (stored in alternating 256-byte halves of 512-byte + /// blocks) with no re-encoding; the FDC reader locks phase on the A1 sync marks in the stream. HFEv3 + /// ("HXCHFEV3") adds an opcode stream and is not handled here. + /// + public static class HfeConverter + { + private const int HeaderSize = 512; + private const int BlockSize = 512; + private const int SideHalf = 256; + + public static bool IsHfe(byte[] d) + => d != null && d.Length >= HeaderSize && Match(d, "HXCPICFE"); + + public static bool IsHfeV3(byte[] d) + => d != null && d.Length >= HeaderSize && Match(d, "HXCHFEV3"); + + private static bool Match(byte[] d, string sig) + { + for (int i = 0; i < 8; i++) if (d[i] != (byte)sig[i]) return false; + return true; + } + + public static FluxDisk ToFluxDisk(byte[] hfe) + { + var (tracks, writeProtected) = Parse(hfe); + var disk = new FluxDisk { WriteProtected = writeProtected }; + foreach (var t in tracks) disk.SetTrack(t.Cylinder, t.Side, t.Track); + return disk; + } + + public static (List Tracks, bool WriteProtected) Parse(byte[] d) + { + if (IsHfeV3(d)) throw new System.ArgumentException("HFEv3 (opcode stream) is not supported", nameof(d)); + if (!IsHfe(d)) throw new System.ArgumentException("not an HFE file (no HXCPICFE signature)", nameof(d)); + + int numTracks = d[0x009]; + int numSides = d[0x00A]; + int lutOffset = ReadLe16(d, 0x012) * BlockSize; + bool writeProtected = d[0x014] == 0x00; // 0x00 = write protected, 0xFF = unprotected + + var tracks = new List(); + for (int t = 0; t < numTracks; t++) + { + int lut = lutOffset + t * 4; + if (lut + 4 > d.Length) break; + int dataOffset = ReadLe16(d, lut) * BlockSize; + int trackLen = ReadLe16(d, lut + 2); + if (dataOffset <= 0 || trackLen <= 0 || dataOffset > d.Length) continue; + + for (int side = 0; side < numSides; side++) + { + byte[] cells = DeinterleaveSide(d, dataOffset, trackLen, side); + if (cells.Length == 0) continue; + tracks.Add(new HfeTrack { Cylinder = t, Side = side, Track = new MfmTrack(cells, cells.Length * 8) }); + } + } + return (tracks, writeProtected); + } + + // Gather one side's cell bytes out of the interleaved 512-byte blocks (side 0 = first 256 bytes of a + // block, side 1 = next 256). The bytes are already LSB-first cells, matching MfmTrack's packing. + private static byte[] DeinterleaveSide(byte[] d, int dataOffset, int trackLen, int side) + { + var outb = new List(trackLen / 2 + SideHalf); + int pos = dataOffset; + int remaining = trackLen; + while (remaining > 0) + { + int block = System.Math.Min(BlockSize, remaining); + int sideStart = pos + (side == 0 ? 0 : SideHalf); + int sideLen = System.Math.Min(SideHalf, System.Math.Max(0, block - (side == 0 ? 0 : SideHalf))); + for (int i = 0; i < sideLen; i++) + { + int p = sideStart + i; + if (p >= d.Length) break; + outb.Add(d[p]); + } + pos += BlockSize; + remaining -= BlockSize; + } + return outb.ToArray(); + } + + private static int ReadLe16(byte[] d, int o) => d[o] | (d[o + 1] << 8); + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/IpfConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/IpfConverter.cs new file mode 100644 index 00000000000..8ff49e47be7 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/IpfConverter.cs @@ -0,0 +1,302 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Kinds of IPF data-area stream element (dataType, 5 LSB of the dataHead byte). + /// + public enum IpfDataType + { + End = 0, + Sync = 1, + Data = 2, + Gap = 3, + Raw = 4, + Fuzzy = 5, + } + + /// + /// One IPF data-stream element, already tokenized (Fuzzy carries no sample - generate random). + /// + public sealed class IpfDataElement + { + public IpfDataType Type; + public int Size; // sample size, in bytes or bits per the block's DataInBit flag + public bool SizeInBits; // interpretation of Size + public byte[] Sample = System.Array.Empty(); // empty for Fuzzy + } + + /// + /// One IPF gap-stream element (GapLength = repeat count, or SampleLength = a bit sample). + /// + public sealed class IpfGapElement + { + public int ElemType; // 1 = GapLength (repeat count), 2 = SampleLength (bit sample) + public int Size; // repeat count (type 1) or sample size in bits (type 2) + public byte[] Sample = System.Array.Empty(); // only for type 2 + } + + /// + /// A DATA-record block descriptor (32 bytes) plus its decoded data-stream elements. + /// + public sealed class IpfBlockDescriptor + { + public int DataBits; + public int GapBits; + public int DataBytesOrGapOffset; // CAPS: dataBytes; SPS: gapOffset + public int GapBytesOrCellType; // CAPS: gapBytes; SPS: cellType + public int EncoderType; // 0 unknown, 1 MFM, 2 raw + public int BlockFlags; // bit0 ForwardGap, bit1 BackwardGap, bit2 DataInBit + public int GapDefault; + public int DataOffset; + + public bool DataInBit => (BlockFlags & 0x04) != 0; + + public List DataElements { get; } = new List(); + } + + /// + /// IPF INFO record. + /// + public sealed class IpfInfo + { + public int MediaType, EncoderType, EncoderRev, FileKey, FileRev, Origin; + public int MinTrack, MaxTrack, MinSide, MaxSide; + public int[] Platforms = new int[4]; + } + + /// + /// IPF IMGE record: describes one track/side and points at a DATA record by DataKey. + /// + public sealed class IpfImage + { + public int Track, Side, Density, SignalType; + public int TrackBytes, StartBytePos, StartBitPos; + public int DataBits, GapBits, TrackBits, BlockCount; + public int TrackFlags, DataKey; + + public bool Fuzzy => (TrackFlags & 0x01) != 0; + } + + /// + /// IPF DATA record: the block descriptors + stream data for the matching IMGE (by DataKey). + /// + public sealed class IpfDataRecord + { + public int DataKey; + public bool ExtraCrcOk; + public List Blocks { get; } = new List(); + } + + /// + /// Parsed IPF file: the INFO block plus IMGE records and DATA records keyed by DataKey. + /// + public sealed class IpfDisk + { + public IpfInfo Info; + public List Images { get; } = new List(); + public Dictionary Data { get; } = new Dictionary(); + public bool AllCrcOk = true; + } + + /// + /// IPF container parser (record layer). Walks the CAPS/INFO/IMGE/DATA records, validates the + /// record and data-block CRC32s, and tokenizes each block's data-stream elements. Rolling the decoded + /// stream into flux cells is a separate step. Big-endian throughout. + /// + public static class IpfConverter + { + public static bool IsIpf(byte[] d) + => d != null && d.Length >= 12 && d[0] == (byte)'C' && d[1] == (byte)'A' && d[2] == (byte)'P' && d[3] == (byte)'S'; + + public static IpfDisk Parse(byte[] d) + { + if (!IsIpf(d)) throw new System.ArgumentException("not an IPF file (no CAPS record)", nameof(d)); + + var disk = new IpfDisk(); + int pos = 0; + while (pos + 12 <= d.Length) + { + string type = System.Text.Encoding.ASCII.GetString(d, pos, 4); + int length = ReadBe(d, pos + 4); + int crc = ReadBe(d, pos + 8); + if (length < 12 || pos + length > d.Length) break; + + uint calc = Crc32Iso.ComputeWithZeroedField(d, pos, length, pos + 8); + bool headerCrcOk = calc == (uint)crc; + if (!headerCrcOk) disk.AllCrcOk = false; + + switch (type) + { + case "CAPS": + break; + case "INFO": + disk.Info = ParseInfo(d, pos + 12); + break; + case "IMGE": + disk.Images.Add(ParseImage(d, pos + 12)); + break; + case "DATA": + pos = ParseData(d, pos, disk); + continue; // ParseData returns the next record position (past the extra data block) + default: + break; // unknown record - skip by length + } + pos += length; + } + return disk; + } + + private static IpfInfo ParseInfo(byte[] d, int o) => new() + { + MediaType = ReadBe(d, o), + EncoderType = ReadBe(d, o + 4), + EncoderRev = ReadBe(d, o + 8), + FileKey = ReadBe(d, o + 12), + FileRev = ReadBe(d, o + 16), + Origin = ReadBe(d, o + 20), + MinTrack = ReadBe(d, o + 24), + MaxTrack = ReadBe(d, o + 28), + MinSide = ReadBe(d, o + 32), + MaxSide = ReadBe(d, o + 36), + Platforms = new[] { ReadBe(d, o + 48), ReadBe(d, o + 52), ReadBe(d, o + 56), ReadBe(d, o + 60) }, + }; + + private static IpfImage ParseImage(byte[] d, int o) => new() + { + Track = ReadBe(d, o), + Side = ReadBe(d, o + 4), + Density = ReadBe(d, o + 8), + SignalType = ReadBe(d, o + 12), + TrackBytes = ReadBe(d, o + 16), + StartBytePos = ReadBe(d, o + 20), + StartBitPos = ReadBe(d, o + 24), + DataBits = ReadBe(d, o + 28), + GapBits = ReadBe(d, o + 32), + TrackBits = ReadBe(d, o + 36), + BlockCount = ReadBe(d, o + 40), + TrackFlags = ReadBe(d, o + 48), + DataKey = ReadBe(d, o + 52), + }; + + // Returns the position of the next record (past the extra data block). + private static int ParseData(byte[] d, int recordStart, IpfDisk disk) + { + int headerLen = ReadBe(d, recordStart + 4); // 28 (record header + data block) + int o = recordStart + 12; // start of the data block + int extraLen = ReadBe(d, o); // Extra Data Block size + int extraCrc = ReadBe(d, o + 8); + int dataKey = ReadBe(d, o + 12); + int extraStart = recordStart + headerLen; // Extra Data Block begins after the 28-byte header + int next = extraStart + extraLen; + if (extraLen <= 0 || next > d.Length) return recordStart + headerLen; + + var rec = new IpfDataRecord { DataKey = dataKey }; + rec.ExtraCrcOk = Crc32Iso.Compute(d, extraStart, extraLen) == (uint)extraCrc; + if (!rec.ExtraCrcOk) disk.AllCrcOk = false; + + // block count comes from the matching IMGE record + int blockCount = 0; + foreach (var img in disk.Images) if (img.DataKey == dataKey) { blockCount = img.BlockCount; break; } + + for (int b = 0; b < blockCount; b++) + { + int bd = extraStart + b * 32; + if (bd + 32 > d.Length) break; + var block = new IpfBlockDescriptor + { + DataBits = ReadBe(d, bd), + GapBits = ReadBe(d, bd + 4), + DataBytesOrGapOffset = ReadBe(d, bd + 8), + GapBytesOrCellType = ReadBe(d, bd + 12), + EncoderType = ReadBe(d, bd + 16), + BlockFlags = ReadBe(d, bd + 20), + GapDefault = ReadBe(d, bd + 24), + DataOffset = ReadBe(d, bd + 28), + }; + if (block.DataBits > 0 && block.DataOffset > 0) + ReadDataElements(d, extraStart + block.DataOffset, next, block); + rec.Blocks.Add(block); + } + + disk.Data[dataKey] = rec; + return next; + } + + // Tokenize a block's data-stream elements until the terminating null dataHead (or the block end). + private static void ReadDataElements(byte[] d, int start, int limit, IpfBlockDescriptor block) + { + int p = start; + while (p < limit) + { + byte head = d[p++]; + if (head == 0) break; // null dataHead terminates the list + int sizeWidth = (head >> 5) & 0x07; + var type = (IpfDataType)(head & 0x1F); + int size = 0; + for (int i = 0; i < sizeWidth && p < limit; i++) size = (size << 8) | d[p++]; + + var elem = new IpfDataElement { Type = type, Size = size, SizeInBits = block.DataInBit }; + if (type != IpfDataType.Fuzzy) + { + int sampleBytes = block.DataInBit ? (size + 7) / 8 : size; + if (sampleBytes < 0 || p + sampleBytes > limit) break; + elem.Sample = new byte[sampleBytes]; + System.Array.Copy(d, p, elem.Sample, 0, sampleBytes); + p += sampleBytes; + } + block.DataElements.Add(elem); + } + } + + private static int ReadBe(byte[] d, int o) + => (d[o] << 24) | (d[o + 1] << 16) | (d[o + 2] << 8) | d[o + 3]; + + // ---- flux generation ---- + + /// + /// Roll one decoded IMGE/DATA track into MFM flux cells. Within an MFM block, Sync/Raw stream samples + /// are raw cells (8 cells per sample byte), Data/Gap samples are MFM-encoded (16 cells per byte), and + /// Fuzzy elements become weak cells. Blocks are laid down in order, each followed by its inter-block + /// gap filled with 0x4E to the recorded gap length. Returns null for an empty/unformatted track. + /// + public static MfmTrack BuildFluxTrack(IpfImage img, IpfDataRecord data) + { + if (img == null || data == null || img.BlockCount == 0 || img.DataBits == 0) return null; + + var w = new MfmTrackWriter(); + foreach (var block in data.Blocks) + { + foreach (var e in block.DataElements) + { + switch (e.Type) + { + case IpfDataType.Sync: + case IpfDataType.Raw: + w.WriteRawCells(e.Sample, e.SizeInBits ? e.Size : e.Size * 8); + break; + case IpfDataType.Data: + case IpfDataType.Gap: + if (e.SizeInBits) w.WriteRawCells(e.Sample, e.Size); + else foreach (var b in e.Sample) w.WriteByte(b); + break; + case IpfDataType.Fuzzy: + w.WriteWeakCells(e.SizeInBits ? e.Size : e.Size * 16); + break; + } + } + EmitGap(w, block.GapBits); + } + return w.Build(); + } + + // Fill an inter-block gap of the given cell length with MFM-encoded 0x4E (the IBM gap byte). + private static void EmitGap(MfmTrackWriter w, int gapCells) + { + int fullBytes = gapCells / 16; + for (int i = 0; i < fullBytes; i++) w.WriteByte(0x4E); + int rem = gapCells - fullBytes * 16; + if (rem > 0) w.WriteRawCells(System.Array.Empty(), rem); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/RawSectorConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/RawSectorConverter.cs new file mode 100644 index 00000000000..42760584f37 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/RawSectorConverter.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Physical geometry for a headerless raw sector image (which carries no metadata of its own). + /// + public sealed class DiskGeometry + { + public int Cylinders { get; set; } + public int Heads { get; set; } = 1; + public int SectorsPerTrack { get; set; } + public int SectorSize { get; set; } = 512; + public int FirstSectorId { get; set; } = 1; + public int Gap3 { get; set; } = 78; + + /// + /// The standard ZX Spectrum +3 / PCW format: 40 cylinders, 1 side, 9x512 sectors from id 1. + /// + public static DiskGeometry Plus3 => new() { Cylinders = 40, Heads = 1, SectorsPerTrack = 9, SectorSize = 512, FirstSectorId = 1 }; + + /// + /// The standard TR-DOS format used by the Beta 128 interface: 80 cylinders, 2 sides, 16x256 sectors + /// numbered from id 1 = 640K. TrdConverter narrows this to 40-track or single-sided variants from the + /// disk-info sector's disk-type byte. + /// + public static DiskGeometry TrDos => new() { Cylinders = 80, Heads = 2, SectorsPerTrack = 16, SectorSize = 256, FirstSectorId = 1 }; + + public int TotalBytes => Cylinders * Heads * SectorsPerTrack * SectorSize; + } + + /// + /// Loads a headerless raw sector image (.img/.raw/.trd-style) into flux using an explicit geometry - the + /// sectors are laid out sequentially cylinder 0 head 0, cylinder 0 head 1, ... each track's sectors in id + /// order. Each track is synthesized as a clean IBM System-34 MFM track. Since the image carries no gap, + /// weak or timing data the result is a clean disk (protected titles need EDSK/IPF/HFE/SCP instead). + /// + public static class RawSectorConverter + { + public static FluxDisk ToFluxDisk(byte[] data, DiskGeometry g) + { + if (data == null) throw new System.ArgumentNullException(nameof(data)); + if (g == null || g.Cylinders <= 0 || g.SectorsPerTrack <= 0 || g.SectorSize <= 0) + throw new System.ArgumentException("invalid geometry", nameof(g)); + + int n = SizeCode(g.SectorSize); + var disk = new FluxDisk(); + int pos = 0; + for (int cyl = 0; cyl < g.Cylinders; cyl++) + { + for (int head = 0; head < g.Heads; head++) + { + var secs = new List(g.SectorsPerTrack); + for (int s = 0; s < g.SectorsPerTrack; s++) + { + var sd = new byte[g.SectorSize]; + int copy = System.Math.Min(g.SectorSize, System.Math.Max(0, data.Length - pos)); + if (copy > 0) System.Array.Copy(data, pos, sd, 0, copy); + pos += g.SectorSize; + secs.Add(new TrackSector { C = (byte)cyl, H = (byte)head, R = (byte)(g.FirstSectorId + s), N = (byte)n, Data = sd }); + } + disk.SetTrack(cyl, head, StandardMfmFormat.BuildStandardTrack(secs, g.Gap3)); + } + } + return disk; + } + + private static int SizeCode(int sectorSize) + { + int n = 0; + while ((128 << n) < sectorSize && n < 7) n++; + return n; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/SclConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/SclConverter.cs new file mode 100644 index 00000000000..f912cbca222 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/SclConverter.cs @@ -0,0 +1,89 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Loads a TR-DOS .SCL disk image into the shared flux model. SCL is a packed catalogue-plus-data form + /// (not a sector image): an 8-byte "SINCLAIR" signature, a 1-byte file count N, N 14-byte catalogue + /// entries (filename, type, two params, sector count - i.e. a TR-DOS directory entry minus its start + /// track/sector), the files' data concatenated in catalogue order, and a 4-byte checksum. We unpack it + /// into a TR-DOS layout - directory on track 0, files laid out sequentially from track 1 - assigning each + /// file its start track/sector and building the disk-info sector, then reuse the .TRD loader to produce + /// the flux disk (zero-padding the unused tail up to the standard 80-cylinder double-sided geometry). + /// + public static class SclConverter + { + private const int SigLen = 8; // "SINCLAIR" + private const int SclEntry = 14; // catalogue entry size in the SCL + private const int TrdEntry = 16; // TR-DOS directory entry size (adds start sector + track) + private const int SectorSize = 256; + private const int SectorsPerTrack = 16; + private const int TrackZeroSectors = SectorsPerTrack; // track 0 = directory (8) + info (1) + spare + private const int InfoOffset = 8 * SectorSize; // disk-info sector = track 0, sector 9 + + public static bool IsScl(byte[] d) + { + if (d == null || d.Length < SigLen + 1) return false; + return d[0] == 'S' && d[1] == 'I' && d[2] == 'N' && d[3] == 'C' + && d[4] == 'L' && d[5] == 'A' && d[6] == 'I' && d[7] == 'R'; + } + + public static FluxDisk ToFluxDisk(byte[] d) => TrdConverter.ToFluxDisk(ToTrd(d)); + + /// + /// Expand an .SCL into a (trimmed) TR-DOS .TRD sector image. + /// + public static byte[] ToTrd(byte[] d) + { + if (!IsScl(d)) throw new System.ArgumentException("not an SCL file (no SINCLAIR signature)", nameof(d)); + + int n = d[SigLen]; + int catOffset = SigLen + 1; + int dataOffset = catOffset + n * SclEntry; + + // total data sectors (byte 13 of each entry) sizes the image + int totalDataSectors = 0; + for (int i = 0; i < n; i++) + { + int e = catOffset + i * SclEntry; + if (e + SclEntry > d.Length) break; + totalDataSectors += d[e + 13]; + } + + int trdSectors = TrackZeroSectors + totalDataSectors; + var trd = new byte[trdSectors * SectorSize]; + + int sectorPtr = SectorsPerTrack; // first data logical sector = track 1, sector 0 + int src = dataOffset; + for (int i = 0; i < n; i++) + { + int e = catOffset + i * SclEntry; + if (e + SclEntry > d.Length) break; + int dst = i * TrdEntry; + + // copy the 14 SCL bytes (filename..sector count), then assign the start sector/track + System.Array.Copy(d, e, trd, dst, SclEntry); + trd[dst + 14] = (byte)(sectorPtr % SectorsPerTrack); + trd[dst + 15] = (byte)(sectorPtr / SectorsPerTrack); + + int secs = d[e + 13]; + int bytes = secs * SectorSize; + int avail = System.Math.Max(0, System.Math.Min(bytes, d.Length - src)); + if (avail > 0) System.Array.Copy(d, src, trd, sectorPtr * SectorSize, avail); + src += bytes; + sectorPtr += secs; + } + + // disk-info sector (track 0, sector 9) + trd[InfoOffset + 0xE1] = (byte)(sectorPtr % SectorsPerTrack); // first free sector + trd[InfoOffset + 0xE2] = (byte)(sectorPtr / SectorsPerTrack); // first free track + trd[InfoOffset + 0xE3] = 0x16; // disk type: 80 track, double sided + trd[InfoOffset + 0xE4] = (byte)n; // number of files + int freeSectors = 80 * 2 * SectorsPerTrack - sectorPtr; // usable sectors still free + trd[InfoOffset + 0xE5] = (byte)(freeSectors & 0xFF); + trd[InfoOffset + 0xE6] = (byte)((freeSectors >> 8) & 0xFF); + trd[InfoOffset + 0xE7] = 0x10; // TR-DOS marker + for (int j = 0; j < 8; j++) trd[InfoOffset + 0xF5 + j] = 0x20; // blank disk label + + return trd; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/ScpConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/ScpConverter.cs new file mode 100644 index 00000000000..30e168f72ed --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/ScpConverter.cs @@ -0,0 +1,254 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Loader for the SuperCard Pro (.scp) flux image format. SCP records raw flux-reversal timings + /// (16-bit big-endian, in 25 ns ticks) per revolution, not decoded cells - so we recover cells with a + /// per-track software PLL (auto-estimate the real bit-cell time, then track it) and emit one flux + /// transition per interval. + /// SCP stores several revolutions of each track (header byte 5). We decode every revolution and let + /// the FDC read whichever recovered the most valid-CRC sectors (a marginal read on one pass is often clean + /// on another), then use the remaining revolutions to recover WEAK/FUZZY bits: a copy-protection weak sector + /// deliberately reads unstably, which shows up as the same sector's data differing from revolution to + /// revolution. Cells whose byte differs across revolutions (in a sector that does not already read cleanly) + /// are flagged weak so the FDC returns unpredictable data there, reproducing the protection - the whole + /// point of a flux-level dump. To avoid corrupting genuinely-stable sectors with our own decode jitter, we + /// only weaken sectors that fail their data CRC (a solid read is left solid). + /// + public static class ScpConverter + { + private const long DefaultCellTimeNs = 2000; // 2 us bit-cell = 250 kbit/s double density + + public static bool IsScp(byte[] d) + => d != null && d.Length >= 16 && d[0] == (byte)'S' && d[1] == (byte)'C' && d[2] == (byte)'P'; + + /// + /// Number of revolutions captured per track (SCP header byte 5). Multi-rev dumps let a + /// marginal read on one revolution be recovered from another, and weak bits to be detected. + /// + public static int RevolutionCount(byte[] d) => d != null && d.Length > 5 ? d[5] : 0; + + public static FluxDisk ToFluxDisk(byte[] d, long cellTimeNs = DefaultCellTimeNs) + { + if (!IsScp(d)) throw new System.ArgumentException("not an SCP file (no SCP signature)", nameof(d)); + + byte flags = d[0x08]; + if ((flags & 0x40) != 0) throw new System.ArgumentException("SCP extended mode not supported", nameof(d)); + int startTrack = d[0x06]; + int endTrack = d[0x07]; + int headMode = d[0x0A]; // 0 = both, 1 = side 0 only, 2 = side 1 only + int resolution = d[0x0B]; + long tickNs = 25L * (resolution + 1); + int revs = d[0x05]; + if (revs < 1) revs = 1; + + var disk = new FluxDisk(); + for (int t = startTrack; t <= endTrack; t++) + { + int tableEntry = 0x10 + t * 4; + if (tableEntry + 4 > d.Length) break; + int tdh = ReadLe32(d, tableEntry); + if (tdh == 0 || tdh + 16 > d.Length) continue; + if (d[tdh] != 'T' || d[tdh + 1] != 'R' || d[tdh + 2] != 'K') continue; + + // decode every revolution once, keeping its cells and its decoded sectors + var revCells = new List<(byte[] Packed, int Count)>(); + var revSectors = new List>(); + for (int rev = 0; rev < revs; rev++) + { + byte[] p = DecodeRevolution(d, tdh, rev, revs, tickNs, cellTimeNs, out int cc); + if (p == null || cc == 0) { revCells.Add((null, 0)); revSectors.Add(new List()); continue; } + revCells.Add((p, cc)); + revSectors.Add(StandardMfmFormat.DecodeSectorLocations(new MfmTrack(p, cc))); + } + + // the FDC reads whichever revolution recovered the most sectors cleanly (a marginal read on one + // pass is often clean on another); rev 0 wins ties. + int baseRev = -1, bestGood = -1; + for (int rev = 0; rev < revs; rev++) + { + if (revCells[rev].Packed == null) continue; + int good = 0; + foreach (var loc in revSectors[rev]) if (loc.Sector.IdCrcOk && loc.Sector.DataCrcOk) good++; + if (good > bestGood) { bestGood = good; baseRev = rev; } + } + if (baseRev < 0) continue; // no revolution decoded + + var (basePacked, baseCells) = revCells[baseRev]; + // weak-cell mask: bytes of a not-cleanly-read sector that differ across revolutions + byte[] weak = revs > 1 ? ComputeWeakMask(revSectors, baseRev, baseCells) : null; + + var (cyl, head) = MapTrack(t, headMode); + disk.SetTrack(cyl, head, new MfmTrack(basePacked, baseCells, weak)); + } + return disk; + } + + /// + /// Decode one revolution's flux into packed MFM cells. Returns null if the revolution is + /// missing/out of range. + /// + private static byte[] DecodeRevolution(byte[] d, int tdh, int rev, int revs, long tickNs, long cellTimeNs, out int cellCount) + { + cellCount = 0; + if (rev < 0 || rev >= revs) return null; + // per-rev triplet after the 4-byte "TRK"+trackNo header: [index-time(4)][flux-count(4)][data-offset(4)] + int tripletBase = tdh + 4 + rev * 12; + if (tripletBase + 12 > d.Length) return null; + int trackLen = ReadLe32(d, tripletBase + 4); // flux transition count + int dataOffset = ReadLe32(d, tripletBase + 8); // relative to the TDH start + if (trackLen <= 0 || tdh + dataOffset >= d.Length) return null; + return FluxToCells(d, tdh + dataOffset, trackLen, tickNs, cellTimeNs, out cellCount); + } + + /// + /// Build a weak-cell mask for the chosen base revolution's track: for each base sector that does NOT + /// read cleanly (its data CRC fails), compare its bytes against the same sector decoded on the other + /// revolutions; bytes that differ across revolutions are weak (the flux there is unstable = the disk's + /// deliberate weak-sector protection). Solid (CRC-OK) sectors are left alone so our own decode jitter + /// cannot corrupt stable data. Returns null if nothing is weak. + /// + private static byte[] ComputeWeakMask(List> revSectors, int baseRev, int baseCells) + { + var baseLocs = revSectors[baseRev]; + if (baseLocs.Count == 0) return null; + + // gather each sector R's data copies across all revolutions + var copies = new Dictionary>(); + for (int rev = 0; rev < revSectors.Count; rev++) + foreach (var loc in revSectors[rev]) + { + if (!copies.TryGetValue(loc.Sector.R, out var l)) { l = new List(); copies[loc.Sector.R] = l; } + l.Add(loc.Sector.Data); + } + + var weakBits = new bool[baseCells]; + bool any = false; + foreach (var loc in baseLocs) + { + // leave solid reads alone; only a sector that already fails CRC is a weak-sector candidate + if (loc.Sector.DataCrcOk) continue; + if (!copies.TryGetValue(loc.Sector.R, out var cps) || cps.Count < 2) continue; + + int size = loc.Sector.SizeBytes; + var b0 = loc.Sector.Data; + for (int j = 0; j < size; j++) + { + // flag this byte's 16 cells weak (data start + j bytes, each byte is 16 cells). Stop at the + // end of the track: an OVERSIZED/overrun sector (e.g. a protection track's single N=6 = 8192 + // -byte sector on a ~6250-byte track) declares far more data than physically fits, so j*16 + // runs past the track. It must NOT wrap - wrapping would flag the start-of-track sync/IDAM + // cells weak and make the sector impossible to find (its own address mark becomes fuzzy). + int cellBase = loc.DataStartCell + j * 16; + if (cellBase + 16 > baseCells) break; + + byte v0 = j < b0.Length ? b0[j] : (byte)0; + bool differs = false; + for (int c = 0; c < cps.Count && !differs; c++) + { + byte vc = j < cps[c].Length ? cps[c][j] : (byte)0; + if (vc != v0) differs = true; + } + if (!differs) continue; + for (int k = 0; k < 16; k++) weakBits[cellBase + k] = true; + any = true; + } + } + if (!any) return null; + + var weak = new byte[(baseCells + 7) >> 3]; + for (int i = 0; i < baseCells; i++) + if (weakBits[i]) weak[i >> 3] |= (byte)(1 << (i & 7)); + return weak; + } + + private static (int Cyl, int Head) MapTrack(int track, int headMode) + // SCP track slots are ALWAYS physical (track = cylinder*2 + head): even slots = side 0, odd = side 1, + // even for a single-sided image (which just leaves the other side's slots empty). So the cylinder is + // always track>>1; headMode only fixes which head a single-sided image's slots belong to. + => headMode switch + { + 1 => (track >> 1, 0), // side 0 only: data lives in the even slots (0,2,4,…) + 2 => (track >> 1, 1), // side 1 only: data lives in the odd slots (1,3,5,…) + _ => (track >> 1, track & 1), // both heads interleaved + }; + + // Decode a track's flux into MFM cells (LSB-first packed, matching MfmTrack) with a software PLL data + // separator. A real drive dump does not run at exactly the nominal cell time (these +3 dumps sit ~2.5% + // slow, ~2050 ns instead of 2000 ns) AND has per-transition jitter. A rigid grid mis-rounds intervals + // near the 2/3-cell boundary; a single slipped cell desyncs the rest of the field, so short ID fields + // survive but long 512-byte data fields fail CRC. The separator therefore (1) seeds a per-track cell + // estimate from the flux itself, then (2) runs a slow INTEGRAL loop that tracks the true bit-cell rate + // - correcting slow spindle/rate drift while AVERAGING OUT the jitter, so a noisy interval does not slip + // a cell. A proportional/high-gain loop instead chases the jitter and slips more (measured on real +3 + // dumps: this slow integral loop recovers markedly more standard sectors than the old proportional nudge). + private static byte[] FluxToCells(byte[] d, int start, int fluxCount, long tickNs, long cellTimeNs, out int cellCount) + { + // gather the flux intervals (ns), folding the SCP 0x0000 overflow markers into the next interval + var flux = new List(fluxCount); + long carry = 0; + int p = start; + for (int i = 0; i < fluxCount; i++) + { + if (p + 2 > d.Length) break; + int v = (d[p] << 8) | d[p + 1]; // 16-bit big-endian + p += 2; + if (v == 0) { carry += 65536; continue; } + flux.Add((carry + v) * tickNs); + carry = 0; + } + + // seed the cell estimate from this track's own flux: total time / total cell count, iterated so the + // classification settles (ignoring long gap/index intervals so they don't skew the average). + double cell = cellTimeNs; + for (int iter = 0; iter < 4; iter++) + { + long sumNs = 0, sumCells = 0; + foreach (var f in flux) + { + int n = (int)(f / cell + 0.5); + if (n < 1) n = 1; + if (n > 5) continue; // skip gaps/index for the rate estimate + sumNs += f; + sumCells += n; + } + if (sumCells == 0) break; + double est = (double)sumNs / sumCells; + // keep the estimate sane (a dump won't be off by more than ~15%) + if (est < cellTimeNs * 0.85) est = cellTimeNs * 0.85; + else if (est > cellTimeNs * 1.15) est = cellTimeNs * 1.15; + cell = est; + } + + // PLL pass: an integral loop tracks the bit-cell period. `centre` (the seed estimate) stays fixed; + // `freq` accumulates the per-cell rate error so the working `period` follows slow rate drift with + // zero steady-state error, while the tiny gain averages out per-transition jitter (a large gain + // would chase the jitter and slip cells). Period clamped +/-20% of the estimate so noise/gaps can't + // drag it away. ki tuned on real +3 dumps (0.01 = best recovery; 0 = fixed grid, higher = jitter-led). + const double ki = 0.01; + double centre = cell, freq = 0.0; + double lo = centre * 0.80, hi = centre * 1.20; + var cells = new List(fluxCount * 3); + foreach (var f in flux) + { + double period = centre + freq; + if (period < lo) period = lo; else if (period > hi) period = hi; + int n = (int)(f / period + 0.5); + if (n < 1) n = 1; + if (n <= 5) // only track the loop on real data intervals, not long gaps/index + freq += ((double)f / n - period) * ki; + for (int c = 0; c < n - 1; c++) cells.Add(false); + cells.Add(true); // flux transition on the final cell + } + + cellCount = cells.Count; + var packed = new byte[(cellCount + 7) >> 3]; + for (int i = 0; i < cellCount; i++) + if (cells[i]) packed[i >> 3] |= (byte)(1 << (i & 7)); // LSB first, matching MfmTrack + return packed; + } + + private static int ReadLe32(byte[] d, int o) => d[o] | (d[o + 1] << 8) | (d[o + 2] << 16) | (d[o + 3] << 24); + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/StandardMfmFormat.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/StandardMfmFormat.cs new file mode 100644 index 00000000000..de060fdc9fe --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/StandardMfmFormat.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Input to track synthesis: one sector's ID + data (what a DSK/EDSK loader produces). + /// + public sealed class TrackSector + { + public byte C, H, R, N; + public byte[] Data = System.Array.Empty(); + public bool Deleted; // write a Deleted DAM (0xF8) instead of a normal DAM (0xFB) + public bool IdCrcError; // corrupt the ID-field CRC (for error synthesis / tests) + public bool DataCrcError; // corrupt the data-field CRC + + /// + /// Multiple recorded data copies for a weak sector (EDSK). When set (length greater than 1), the + /// synthesized track writes copy 0 but flags the byte positions that differ across copies as weak, + /// so the FDC reads them unpredictably - the native mechanism behind weak-sector copy protection. + /// + public byte[][] WeakCopies; + + public int SizeBytes => 128 << (N & 7); + } + + /// + /// Decoded result of reading a sector back off a track. + /// + public sealed class DecodedSector + { + public byte C, H, R, N; + public byte[] Data = System.Array.Empty(); + public bool HasData; + public bool IdCrcOk; + public bool DataCrcOk; + public bool Deleted; + + public int SizeBytes => 128 << (N & 7); + } + + /// + /// Synthesises a clean IBM System-34 MFM track from a sector list (the DSK/EDSK -> flux path) and reads + /// sectors back off a track (the FDC-side decode). This is the Phase-1 de-risk: a DSK sector list -> + /// real MFM track (proper A1 sync marks, IDAM/DAM, computed CRCs) -> decode -> identical CHRN/data/CRC. + /// Note (design): a plain DSK carries no weak/gap/timing data, so this yields a *clean* track. Weak/ + /// fuzzy/protection data comes from richer formats (EDSK multi-copy, IPF) at a higher layer, not here. + /// + public static class StandardMfmFormat + { + // IBM System-34 MFM gap/sync sizes (bytes) + private const int Gap4a = 80, Sync = 12, Gap1 = 50, Gap2 = 22, Gap4b = 40; + private const byte GapByte = 0x4E, SyncByte = 0x00; + private const byte IamMark = 0xFC, IdamMark = 0xFE, DamMark = 0xFB, DeletedDamMark = 0xF8; + + public static MfmTrack BuildStandardTrack(IReadOnlyList sectors, int gap3 = 78) + { + var w = new MfmTrackWriter(); + + // Gap 4a, then sync + Index Address Mark (C2 C2 C2 FC), then Gap 1 + w.WriteBytes(GapByte, Gap4a); + w.WriteBytes(SyncByte, Sync); + w.WriteSyncC2(); w.WriteSyncC2(); w.WriteSyncC2(); + w.WriteByte(IamMark); + w.WriteBytes(GapByte, Gap1); + + foreach (var s in sectors) + { + // --- ID field: sync + (A1 A1 A1 FE C H R N CRC) --- + w.WriteBytes(SyncByte, Sync); + ushort crc = Crc16Ccitt.Init; + crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); + w.WriteSyncA1(); w.WriteSyncA1(); w.WriteSyncA1(); + crc = Feed(crc, IdamMark); w.WriteByte(IdamMark); + crc = Feed(crc, s.C); w.WriteByte(s.C); + crc = Feed(crc, s.H); w.WriteByte(s.H); + crc = Feed(crc, s.R); w.WriteByte(s.R); + crc = Feed(crc, s.N); w.WriteByte(s.N); + WriteCrc(w, crc, s.IdCrcError); + w.WriteBytes(GapByte, Gap2); + + // --- Data field: sync + (A1 A1 A1 DAM data CRC) --- + w.WriteBytes(SyncByte, Sync); + crc = Crc16Ccitt.Init; + crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); + w.WriteSyncA1(); w.WriteSyncA1(); w.WriteSyncA1(); + byte dam = s.Deleted ? DeletedDamMark : DamMark; + crc = Feed(crc, dam); w.WriteByte(dam); + + int size = s.SizeBytes; + // A byte position is weak if the recorded copies disagree there. + bool[] weakByte = WeakMask(s.WeakCopies, size); + byte[] src = s.WeakCopies is { Length: > 0 } ? s.WeakCopies[0] : s.Data; + for (int i = 0; i < size; i++) + { + byte d = i < src.Length ? src[i] : (byte)0x00; + crc = Feed(crc, d); + if (weakByte != null && weakByte[i]) w.WriteByteWeak(d); + else w.WriteByte(d); + } + WriteCrc(w, crc, s.DataCrcError); + w.WriteBytes(GapByte, gap3); + } + + w.WriteBytes(GapByte, Gap4b); + return w.Build(); + } + + /// + /// Decode a track back into a TrackSector list (the input form for re-synthesis). Used by the write + /// path: read the current track, modify the target sector(s), then rebuild. Preserves each sector's + /// deleted mark and CRC-error state; weak data is not reconstructed (a rewrite makes the track clean). + /// + public static List ToTrackSectors(MfmTrack track, WeakBitRng weakRng = null) + { + var list = new List(); + if (track == null) return list; + foreach (var d in DecodeSectors(track, weakRng)) + { + list.Add(new TrackSector + { + C = d.C, H = d.H, R = d.R, N = d.N, + Data = d.Data, + Deleted = d.Deleted, + IdCrcError = !d.IdCrcOk, + DataCrcError = !d.DataCrcOk, + }); + } + return list; + } + + public static List DecodeSectors(MfmTrack track, WeakBitRng weakRng = null) + { + var list = new List(); + foreach (var loc in DecodeSectorLocations(track, weakRng)) list.Add(loc.Sector); + return list; + } + + /// + /// A decoded sector plus where its data field sits on the track, in cells (the first data byte + /// starts at DataStartCell, each subsequent byte 16 cells later). Used to map a sector's + /// bytes back to track cells - e.g. to flag weak/fuzzy cells derived from cross-revolution comparison. + /// + public sealed class SectorLocation + { + public DecodedSector Sector; + public int DataStartCell; + } + + /// + /// Like DecodeSectors but also reports each sector's data-field start cell. + /// + public static List DecodeSectorLocations(MfmTrack track, WeakBitRng weakRng = null) + { + var r = new MfmTrackReader(track); + if (weakRng != null) r.WeakRng = weakRng; // shared RNG so repeated weak reads vary + var list = new List(); + var seen = new HashSet(); + int pos = 0; + DecodedSector pending = null; + + for (; ; ) + { + if (!r.TryFindAddressMark(ref pos, out byte mark, out _, out int syncStart)) + break; + // one full revolution done once we return to a mark we've already decoded + if (!seen.Add(syncStart)) + break; + + if (mark == IdamMark) + { + var chrn = r.ReadBytes(ref pos, 4); + var crcBytes = r.ReadBytes(ref pos, 2); + + ushort read = (ushort)((crcBytes[0] << 8) | crcBytes[1]); + ushort calc = IdFieldCrc(chrn[0], chrn[1], chrn[2], chrn[3]); + pending = new DecodedSector + { + C = chrn[0], H = chrn[1], R = chrn[2], N = chrn[3], + IdCrcOk = read == calc, + }; + } + else if (mark == DamMark || mark == DeletedDamMark) + { + int n = pending?.N ?? 2; + int size = 128 << (n & 7); + int dataStart = pos; // cell position of the first data byte (before ReadBytes advances pos) + var data = r.ReadBytes(ref pos, size); + var crcBytes = r.ReadBytes(ref pos, 2); + + ushort read = (ushort)((crcBytes[0] << 8) | crcBytes[1]); + ushort calc = DataFieldCrc(mark, data); + if (pending != null) + { + pending.Data = data; + pending.HasData = true; + pending.DataCrcOk = read == calc; + pending.Deleted = mark == DeletedDamMark; + list.Add(new SectorLocation { Sector = pending, DataStartCell = dataStart }); + pending = null; + } + } + } + return list; + } + + /// + /// Read a specific sector (matched on full CHRN) off a track, the way the FDC Read Data command + /// locates it. Returns null if no matching sector ID is present on the track. + /// + public static DecodedSector ReadSectorById(MfmTrack track, byte c, byte h, byte r, byte n, WeakBitRng weakRng = null) + { + if (track == null) return null; + foreach (var s in DecodeSectors(track, weakRng)) + if (s.C == c && s.H == h && s.R == r && s.N == n) + return s; + return null; + } + + private static ushort Feed(ushort crc, byte b) => Crc16Ccitt.Update(crc, b); + + private static void WriteCrc(MfmTrackWriter w, ushort crc, bool corrupt) + { + if (corrupt) crc ^= 0xFFFF; + w.WriteByte((byte)(crc >> 8)); + w.WriteByte((byte)(crc & 0xFF)); + } + + /// + /// The CRC-16-CCITT of an ID field (computed over the three A1 sync bytes, the IDAM, then C H R N), + /// as recorded on the disk and returned by a WD179X Read Address command. Public so a controller can + /// report the real CRC bytes. + /// + public static ushort IdFieldCrc(byte c, byte h, byte r, byte n) + { + ushort crc = Crc16Ccitt.Init; + crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); + crc = Feed(crc, IdamMark); + crc = Feed(crc, c); crc = Feed(crc, h); crc = Feed(crc, r); crc = Feed(crc, n); + return crc; + } + + private static ushort DataFieldCrc(byte mark, byte[] data) + { + ushort crc = Crc16Ccitt.Init; + crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); crc = Feed(crc, 0xA1); + crc = Feed(crc, mark); + foreach (var b in data) crc = Feed(crc, b); + return crc; + } + + // A byte position is weak where the recorded copies disagree. + private static bool[] WeakMask(byte[][] copies, int size) + { + if (copies == null || copies.Length < 2) return null; + var mask = new bool[size]; + for (int i = 0; i < size; i++) + { + byte first = i < copies[0].Length ? copies[0][i] : (byte)0; + for (int c = 1; c < copies.Length; c++) + { + byte v = i < copies[c].Length ? copies[c][i] : (byte)0; + if (v != first) { mask[i] = true; break; } + } + } + return mask; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/TrdConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/TrdConverter.cs new file mode 100644 index 00000000000..7c4e5d22787 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/TrdConverter.cs @@ -0,0 +1,48 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Loads a TR-DOS .TRD disk image (the Beta 128 / Pentagon native format) into the shared flux model. + /// A .TRD is a headerless raw sector dump: 16 sectors of 256 bytes per track, stored in TR-DOS logical + /// track order (cylinder-major, side-minor: cyl0/side0, cyl0/side1, cyl1/side0, ...), which is exactly + /// the layout RawSectorConverter emits. The geometry (40/80 cylinders, single/double sided) is taken + /// from the disk-type byte in the disk-info sector, falling back to 80-track double-sided (640K). + /// + public static class TrdConverter + { + // disk-info sector = track 0, sector 9 (zero-based sector index 8) -> file offset 8 * 256 = 2048 + private const int InfoSectorOffset = 8 * 256; + private const int DiskTypeOffset = InfoSectorOffset + 0xE3; // disk type byte + private const int TrDosIdOffset = InfoSectorOffset + 0xE7; // TR-DOS marker byte (0x10) + + /// + /// A .TRD carries no signature, so validate structurally: the length must be a whole number of + /// 256-byte sectors (TOSEC and other tools commonly store TRIMMED images, omitting the trailing + /// unused sectors - so the length is NOT necessarily a whole number of 16-sector tracks), it must be + /// large enough to hold track 0, and the disk-info sector must carry the TR-DOS marker (0x10) and a + /// known disk-type byte (0x16-0x19). The trailing sectors omitted by a trim are zero-padded on load. + /// + public static bool IsTrd(byte[] d) + { + if (d == null || d.Length < 9 * 256) return false; + if (d.Length % 256 != 0) return false; + byte type = d[DiskTypeOffset]; + return d[TrDosIdOffset] == 0x10 && type >= 0x16 && type <= 0x19; + } + + public static FluxDisk ToFluxDisk(byte[] d) + { + var g = DiskGeometry.TrDos; + if (d != null && d.Length > DiskTypeOffset) + { + switch (d[DiskTypeOffset]) + { + case 0x16: g.Cylinders = 80; g.Heads = 2; break; + case 0x17: g.Cylinders = 40; g.Heads = 2; break; + case 0x18: g.Cylinders = 80; g.Heads = 1; break; + case 0x19: g.Cylinders = 40; g.Heads = 1; break; + } + } + return RawSectorConverter.ToFluxDisk(d, g); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Converters/UdiConverter.cs b/src/BizHawk.Emulation.Cores/Floppy/Converters/UdiConverter.cs new file mode 100644 index 00000000000..0a2fb46ea05 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Converters/UdiConverter.cs @@ -0,0 +1,62 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Loader for the ZX Spectrum UDI v1.0 ("UDI!") disk image format. UDI is a track-level format: each + /// track stores the decoded byte stream as read off the track (gaps, sync, address marks, CHRN, CRC and + /// data) plus a clock bitmap flagging which bytes are anomalous-clock markers (the A1/C2 sync bytes). We + /// map that straight onto MFM cells - a flagged 0xA1/0xC2 becomes the missing-clock sync, every other + /// byte is MFM-encoded normally - which the FDC reader then decodes as usual. The compressed variant + /// ("udi!") is not supported. + /// + public static class UdiConverter + { + public static bool IsUdi(byte[] d) + => d != null && d.Length >= 16 + && (d[0] == 'U' || d[0] == 'u') && (d[1] == 'D' || d[1] == 'd') && (d[2] == 'I' || d[2] == 'i') && d[3] == '!' + && d[0x08] == 0; // uppercase "UDI!" = uncompressed, lowercase "udi!" = compressed (rejected on convert) + + public static FluxDisk ToFluxDisk(byte[] d) + { + if (!IsUdi(d)) throw new System.ArgumentException("not a UDI v1.0 file", nameof(d)); + if (d[0] == 'u') throw new System.ArgumentException("compressed UDI ('udi!') is not supported", nameof(d)); + + int numTracks = d[0x09] + 1; + int numSides = d[0x0A] + 1; + int extHeader = ReadLe32(d, 0x0C); + int pos = 0x10 + extHeader; + + var disk = new FluxDisk(); + int total = numTracks * numSides; + for (int t = 0; t < total; t++) + { + if (pos + 3 > d.Length) break; + pos++; // track type (0 = MFM); ZX/Beta tracks are MFM + int tlen = d[pos] | (d[pos + 1] << 8); + pos += 2; + int clen = (tlen + 7) / 8; // one clock bit per track byte + if (tlen <= 0 || pos + tlen + clen > d.Length) { pos += tlen + clen; continue; } + + // tracks are stored interleaved by side within a cylinder + disk.SetTrack(t / numSides, t % numSides, BuildTrack(d, pos, tlen, pos + tlen)); + pos += tlen + clen; + } + return disk; + } + + private static MfmTrack BuildTrack(byte[] d, int dataStart, int tlen, int clockStart) + { + var w = new MfmTrackWriter(); + for (int i = 0; i < tlen; i++) + { + byte b = d[dataStart + i]; + bool marker = (d[clockStart + (i >> 3)] & (1 << (i & 7))) != 0; // clock bitmap: LSB first + if (marker && b == 0xA1) w.WriteSyncA1(); + else if (marker && b == 0xC2) w.WriteSyncC2(); + else w.WriteByte(b); + } + return w.Build(); + } + + private static int ReadLe32(byte[] d, int o) => d[o] | (d[o + 1] << 8) | (d[o + 2] << 16) | (d[o + 3] << 24); + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Crc16Ccitt.cs b/src/BizHawk.Emulation.Cores/Floppy/Crc16Ccitt.cs new file mode 100644 index 00000000000..1a3adf74f64 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Crc16Ccitt.cs @@ -0,0 +1,40 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// CRC-16/CCITT (polynomial 0x1021, init 0xFFFF, no reflection, no final XOR) as used by the IBM + /// System-34 MFM (and FM) floppy track format for both the ID field and the data field. The three A1 + /// sync bytes and the address-mark byte are part of the CRC, so a caller seeds with 0xFFFF and feeds + /// A1, A1, A1, the mark byte, then the field bytes, before comparing the trailing two CRC bytes. + /// Part of the shared floppy disk subsystem. + /// + public static class Crc16Ccitt + { + public const ushort Init = 0xFFFF; + + /// + /// Fold one byte into a running CRC (MSB-first). + /// + public static ushort Update(ushort crc, byte b) + { + crc ^= (ushort)(b << 8); + for (int i = 0; i < 8; i++) + { + crc = (crc & 0x8000) != 0 + ? (ushort)((crc << 1) ^ 0x1021) + : (ushort)(crc << 1); + } + return crc; + } + + /// + /// Compute the CRC over a span of bytes. + /// + public static ushort Compute(System.ReadOnlySpan data, ushort init = Init) + { + ushort crc = init; + foreach (var b in data) + crc = Update(crc, b); + return crc; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Crc32Iso.cs b/src/BizHawk.Emulation.Cores/Floppy/Crc32Iso.cs new file mode 100644 index 00000000000..8aebafc7b2e --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Crc32Iso.cs @@ -0,0 +1,49 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Standard CRC-32/ISO-HDLC (the common zlib CRC32): reflected polynomial 0xEDB88320, initial value + /// 0xFFFFFFFF, final XOR 0xFFFFFFFF. Used to verify IPF records and their data blocks. + /// + public static class Crc32Iso + { + private static readonly uint[] Table = BuildTable(); + + private static uint[] BuildTable() + { + var t = new uint[256]; + for (uint i = 0; i < 256; i++) + { + uint c = i; + for (int k = 0; k < 8; k++) + c = (c & 1) != 0 ? 0xEDB88320 ^ (c >> 1) : c >> 1; + t[i] = c; + } + return t; + } + + public static uint Compute(byte[] data, int offset, int length) + { + uint crc = 0xFFFFFFFF; + int end = offset + length; + for (int i = offset; i < end; i++) + crc = Table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8); + return crc ^ 0xFFFFFFFF; + } + + /// + /// CRC over a record whose own 4-byte CRC field (at crcFieldOffset) is treated as zero, matching how + /// IPF stores the value (computed with the field cleared, then written back). + /// + public static uint ComputeWithZeroedField(byte[] data, int offset, int length, int crcFieldOffset) + { + uint crc = 0xFFFFFFFF; + int end = offset + length; + for (int i = offset; i < end; i++) + { + byte b = i >= crcFieldOffset && i < crcFieldOffset + 4 ? (byte)0 : data[i]; + crc = Table[(crc ^ b) & 0xFF] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFF; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/DiskImageLoader.cs b/src/BizHawk.Emulation.Cores/Floppy/DiskImageLoader.cs new file mode 100644 index 00000000000..d45a415e0c9 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/DiskImageLoader.cs @@ -0,0 +1,25 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Detects a disk image's format by signature and converts it into the shared flux model. Recognizes + /// CPC DSK/EDSK, IPF, HxC HFE, SuperCard Pro SCP and ZX Spectrum FDI; a headerless image falls back to a + /// supplied raw geometry (default the standard +3 layout). + /// + public static class DiskImageLoader + { + public static FluxDisk ToFluxDisk(byte[] data, DiskGeometry rawFallback = null) + { + if (data == null || data.Length < 8) throw new System.ArgumentException("empty or too-small disk image", nameof(data)); + + if (IpfConverter.IsIpf(data)) return FluxDisk.FromIpf(data); + if (HfeConverter.IsHfe(data) || HfeConverter.IsHfeV3(data)) return FluxDisk.FromHfe(data); + if (ScpConverter.IsScp(data)) return FluxDisk.FromScp(data); + if (UdiConverter.IsUdi(data)) return FluxDisk.FromUdi(data); + if (FdiConverter.IsFdi(data)) return FluxDisk.FromFdi(data); + if (CpcDskConverter.IsCpcDsk(data)) return FluxDisk.FromCpcDsk(data); + + // headerless: treat as a raw sector image with the given (or standard +3) geometry + return RawSectorConverter.ToFluxDisk(data, rawFallback ?? DiskGeometry.Plus3); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/DiskProtection.cs b/src/BizHawk.Emulation.Cores/Floppy/DiskProtection.cs new file mode 100644 index 00000000000..ed111feda24 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/DiskProtection.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// A recognized +3/CPC disk copy-protection / special-format scheme (None if nothing matched). + /// + public enum DiskProtectionScheme + { + None, + Speedlock, + OperaSoft, + LongTrack, + Alkatraz, + PaulOwens, + Hexagon, + ShadowOfTheBeast, + RainbowArts, + Kbi, + Kbi19, + Prehistorik, + LogoProfessor, + ElevenSector, + } + + /// + /// Identifies well-known ZX Spectrum +3 / Amstrad CPC disk copy-protection and special-format schemes + /// from the flux, for logging. Detection is reported for everything recognized even when the flux model + /// already reproduces the protection (deleted marks, non-standard ids/sizes, weak/fuzzy data) so no + /// mitigation is needed; only Speedlock additionally needs weak-sector synthesis, and only for plain + /// dumps that lack the weak data (see CpcDskConverter). Detection signatures follow the documented + /// on-disk fingerprints (SAMdisk / CPCWiki / the original ZXHawk detection); this is an independent + /// implementation of those facts. + /// + public static class DiskProtection + { + public static string DisplayName(DiskProtectionScheme scheme) + => scheme == DiskProtectionScheme.None ? "None (or unknown)" : scheme.ToString(); + + /// + /// Detect a protection / special-format scheme from the flux (best-effort, for reporting). + /// + public static DiskProtectionScheme Detect(FluxDisk disk) + { + if (disk == null) return DiskProtectionScheme.None; + + var t0 = Decode(disk, 0); + + // signature-string / distinctive-id schemes first (most reliable) + if (IsSpeedlock(t0.Count, t0.Count > 0 ? t0[0].Data : null)) return DiskProtectionScheme.Speedlock; + foreach (var s in t0) + { + if (ContainsAscii(s.Data, "PAUL OWENS")) return DiskProtectionScheme.PaulOwens; + if (ContainsAscii(s.Data, "GON DISK PROT")) return DiskProtectionScheme.Hexagon; + } + if (IsRainbowArts(t0)) return DiskProtectionScheme.RainbowArts; + if (IsKbi(t0)) return DiskProtectionScheme.Kbi; + if (IsShadowOfTheBeast(t0, Decode(disk, 1))) return DiskProtectionScheme.ShadowOfTheBeast; + if (IsOperaSoft(Decode(disk, 40)) || IsOperaSoft(t0)) return DiskProtectionScheme.OperaSoft; + if (IsKbi19(t0)) return DiskProtectionScheme.Kbi19; + if (IsLogoProfessor(t0)) return DiskProtectionScheme.LogoProfessor; + if (IsElevenSector(t0)) return DiskProtectionScheme.ElevenSector; + + // structural schemes that may sit on a non-standard track near the disk start + int scan = System.Math.Min(disk.Cylinders, 12); + for (int c = 0; c < scan; c++) + { + var t = Decode(disk, c); + if (IsPrehistorik(t)) return DiskProtectionScheme.Prehistorik; + if (t.Count == 18 && t[0].C >= 0xE0) return DiskProtectionScheme.Alkatraz; + foreach (var s in t) if (s.N >= 6) return DiskProtectionScheme.LongTrack; // 6K/8K "long" sector + } + + return DiskProtectionScheme.None; + } + + /// + /// The Speedlock track-0 fingerprint: a 9-sector track whose first sector carries the signature. + /// + public static bool IsSpeedlock(int sectorCount, byte[] firstSectorData) + => sectorCount == 9 && ContainsAscii(firstSectorData, "SPEEDLOCK"); + + /// + /// The weak byte range within the Speedlock weak sector (id 2). If the sector starts with filler the + /// weak run is 32 bytes at 0x150, otherwise the whole 512 bytes are treated as weak. + /// + public static (int Offset, int Length) SpeedlockWeakRegion(byte[] data) + { + bool startFiller = true; + for (int i = 0; i < 250 && i + 1 < data.Length; i++) + { + if (data[i] != data[i + 1]) { startFiller = false; break; } + } + return startFiller ? (0x150, 0x20) : (0, System.Math.Min(0x200, data.Length)); + } + + // Rainbow Arts: 9-sector track with a non-standard sector id 198 carrying a data CRC error. + private static bool IsRainbowArts(List t) + { + if (t.Count != 9) return false; + foreach (var s in t) if (s.R == 198 && !s.DataCrcOk) return true; + return false; + } + + // KBI weak sector: a 10-sector track whose final 256-byte sector has a data CRC error and starts "Kxx". + private static bool IsKbi(List t) + { + if (t.Count != 10) return false; + var last = t[t.Count - 1]; + return last.N == 1 && !last.DataCrcOk && last.Data.Length >= 3 + && last.Data[0] == (byte)'K' && IsAlpha(last.Data[1]) && IsAlpha(last.Data[2]); + } + + // KBI-19: an unusual 19- or 20-sector track. + private static bool IsKbi19(List t) => t.Count == 19 || t.Count == 20; + + // OperaSoft 32K: 9 sectors ids 0..8 where the last (id 8) is a 32K sector (size code 8). + private static bool IsOperaSoft(List t) + { + if (t.Count != 9) return false; + foreach (var s in t) if (s.R == 8 && s.N == 8) return true; + return false; + } + + // Prehistorik: a 4K sector (size code 5) with a data CRC error and a "Titus" signature. + private static bool IsPrehistorik(List t) + { + foreach (var s in t) if (s.N == 5 && ContainsAscii(s.Data, "Titus")) return true; + return false; + } + + // Logo Professor: a 10/11-sector track whose sector ids start at 2 (non-standard). + private static bool IsLogoProfessor(List t) + => (t.Count == 10 || t.Count == 11) && LowestId(t) == 2; + + // A full 11-sector track (ids from 1) - a non-standard tight format. + private static bool IsElevenSector(List t) + => t.Count == 11 && LowestId(t) == 1; + + private static bool IsShadowOfTheBeast(List t0, List t1) + { + if (t0.Count != 9 || t1.Count != 8) return false; + for (int i = 0; i < 9; i++) if (t0[i].R != 65 + i) return false; + for (int i = 0; i < 8; i++) if (t1[i].R != 17 + i) return false; + return true; + } + + private static int LowestId(List t) + { + int min = 0xFF; + foreach (var s in t) if (s.R < min) min = s.R; + return min; + } + + private static bool IsAlpha(byte b) => (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z'); + + private static List Decode(FluxDisk disk, int cyl) + { + var t = disk.GetTrack(cyl, 0); + return t == null ? new List() : StandardMfmFormat.DecodeSectors(t); + } + + private static bool ContainsAscii(byte[] data, string s) + { + if (data == null || data.Length < s.Length) return false; + for (int i = 0; i <= data.Length - s.Length; i++) + { + int j = 0; + for (; j < s.Length; j++) if (data[i + j] != (byte)s[j]) break; + if (j == s.Length) return true; + } + return false; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Drives/Eme150Drive.cs b/src/BizHawk.Emulation.Cores/Floppy/Drives/Eme150Drive.cs new file mode 100644 index 00000000000..c24c7f918e3 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Drives/Eme150Drive.cs @@ -0,0 +1,36 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// The Panasonic/Matsushita EME-150: the single-head (single-sided) 3-inch compact floppy disk drive + /// fitted to the early Amstrad CPC (664/6128). The ZX Spectrum +3 and later CPCs use the EME-156, a minor + /// cost-reduced revision whose electrical, timing and track specifications are identical (see + /// Eme156Drive) - only mechanical details (the write-protect plunger and PCB/worm-drive layout) differ. + /// The double-head variant is the EME-250; single-head machines handle double-sided disks by splitting + /// them into two single-sided images at load time. + /// Datasheet figures below come from the EME-150 service manual (Table 1-1, Figure 6): + /// https://www.cpcwiki.eu/imgs/0/0e/EME150_ServiceManual.pdf + /// - 300 rpm rotation (200 ms per revolution; 100 ms average latency), one index pulse per revolution + /// - 1.0 s max motor spin-up before valid read data + /// - 12 ms track-to-track access (a floor on the controller's programmed step rate) + /// - 15 ms head settling time after the final step of a seek + /// - 40 cylinders, single side, 250 kbit/s MFM (double density) + /// + public sealed class Eme150Drive : FloppyDrive + { + public Eme150Drive() : base(Profile) { } + + /// + /// The shared EME-150/156 family profile. No standalone EME-156 datasheet exists; the EME-150 manual + /// is authoritative for both, as the electrical/timing/geometry figures are identical. + /// + public static FloppyDriveProfile Profile { get; } = new() + { + Cylinders = 40, + Sides = 1, + Rpm = 300, + SpinUpMs = 1000, + TrackToTrackMs = 12, + SettleMs = 15, + }; + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/Drives/Eme156Drive.cs b/src/BizHawk.Emulation.Cores/Floppy/Drives/Eme156Drive.cs new file mode 100644 index 00000000000..d5bb0876428 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/Drives/Eme156Drive.cs @@ -0,0 +1,18 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// The Panasonic/Matsushita EME-156: the single-head 3-inch drive fitted to the ZX Spectrum +3 (as the + /// "FD-1") and later Amstrad CPCs. It is a cost-reduced revision of the EME-150 (see Eme150Drive); the + /// two are electrically and timing-wise identical (40 tracks, single sided, 300 rpm, 250 kbit/s MFM), so + /// this reuses the EME-150 family profile. The differences are purely mechanical - the EME-156 senses + /// write-protect via a loose plunger pin rather than a leaf switch, and relocates the stepper worm drive + /// / PCB traces - none of which affect emulation. + /// No standalone EME-156 service manual was published; the EME-150 manual is the authoritative timing + /// source (https://www.cpcwiki.eu/imgs/0/0e/EME150_ServiceManual.pdf), and the EME-156 board schematic + /// appears as a supplement in the ZX Spectrum +3 service manual (FD-1 EME-156 Disk Drive Circuit Diagram). + /// + public sealed class Eme156Drive : FloppyDrive + { + public Eme156Drive() : base(Eme150Drive.Profile) { } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/FloppyDrive.cs b/src/BizHawk.Emulation.Cores/Floppy/FloppyDrive.cs new file mode 100644 index 00000000000..7a5529346bb --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/FloppyDrive.cs @@ -0,0 +1,179 @@ +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// The signals a floppy drive presents to the controller. Different drive types (single-sided 3" for + /// the +3/CPC, others later) implement this so the controller stays hardware-agnostic. + /// + public interface IFloppyDrive + { + bool MotorOn { get; set; } + bool Ready { get; } + bool WriteProtected { get; } + bool Track0 { get; } + int CurrentCylinder { get; } + int SideCount { get; } + + /// + /// True while the motor is spun up to operating speed (rotation/index only run then). + /// + bool AtSpeed { get; } + + /// + /// True during the index-hole pulse window once per revolution (only while AtSpeed). + /// + bool Index { get; } + + /// + /// Mechanical track-to-track step time; a floor on how fast the controller can step. 0 = none. + /// + int TrackToTrackMs { get; } + + /// + /// Head settling time added after the final step of a seek before it completes. 0 = none. + /// + int SettleMs { get; } + + void Step(bool towardHigherCylinder); + void SeekTo(int cylinder); + MfmTrack CurrentTrack(int side); + + /// + /// Replace the flux at the current cylinder / given side (Write Data, Format). + /// + void WriteTrack(int side, MfmTrack track); + + /// + /// Precompute rotation/spin-up thresholds for the host CPU clock (T-states per second). + /// + void ConfigureTiming(long cpuClockHz); + + /// + /// Advance the mechanical timing by the given number of host CPU cycles. + /// + void Clock(int cpuCycles); + } + + /// + /// The fixed mechanical characteristics of a physical drive model: geometry (cylinders, heads), + /// rotational speed, motor spin-up and the track-to-track / settling access times. A concrete drive + /// type (e.g. the EME-150) supplies one of these so its real datasheet figures drive the timing. + /// + public sealed class FloppyDriveProfile + { + public int Cylinders { get; set; } // recorded reference geometry; 0 = unspecified + public int Sides { get; set; } = 2; // physical head count + public int Rpm { get; set; } = 300; // rotational speed + public int SpinUpMs { get; set; } = 1000; // motor start to at-speed + public int TrackToTrackMs { get; set; } // step-to-step access time (0 = no floor) + public int SettleMs { get; set; } // head settle after the final step (0 = none) + + /// + /// A permissive generic profile (no cylinder limit, no step floor or settle). + /// + public static FloppyDriveProfile Generic { get; } = new(); + } + + /// + /// A single mechanical floppy drive holding a FluxDisk. Models head position (current cylinder); the + /// motor, ready, write-protect and track-0 signals; and mechanical timing driven by a FloppyDriveProfile + /// - motor spin-up, disk rotation and the once-per-revolution index pulse. Seek-step timing is driven by + /// the controller (it issues the step pulses at the programmed step rate, bounded by the drive's + /// track-to-track figure); the drive just moves the head. Concrete hardware drives subclass this and pass + /// their datasheet profile. + /// + public class FloppyDrive : IFloppyDrive + { + private readonly FloppyDriveProfile _profile; + + public FloppyDrive() : this(FloppyDriveProfile.Generic) { } + + public FloppyDrive(FloppyDriveProfile profile) => _profile = profile ?? FloppyDriveProfile.Generic; + + public FluxDisk Disk { get; set; } + public bool MotorOn { get; set; } + public int CurrentCylinder { get; private set; } + + public bool WriteProtected => Disk != null && Disk.WriteProtected; + public bool Track0 => CurrentCylinder == 0; + public bool Ready => Disk != null && MotorOn && Disk.Cylinders > 0; + public int SideCount => Disk?.Sides ?? 0; + + public int TrackToTrackMs => _profile.TrackToTrackMs; + public int SettleMs => _profile.SettleMs; + + /// + /// Recorded cylinder count for this drive model (0 if unspecified). + /// + public int CylinderCount => _profile.Cylinders; + + private long _cpuHz = 3_546_900; + private int _cyclesPerRev; + private int _cyclesSpinUp; + private int _indexWindow; + private int _rotation; // position within a revolution, in CPU cycles + private int _spinUp; // elapsed spin-up, in CPU cycles + + public bool AtSpeed => _cyclesSpinUp > 0 && _spinUp >= _cyclesSpinUp; + public bool Index => AtSpeed && _rotation < _indexWindow; + + public void ConfigureTiming(long cpuClockHz) + { + _cpuHz = cpuClockHz > 0 ? cpuClockHz : 3_546_900; + int rpm = _profile.Rpm > 0 ? _profile.Rpm : 300; + _cyclesPerRev = (int)(_cpuHz * 60 / rpm); // e.g. 300 rpm -> 200 ms per revolution + if (_cyclesPerRev < 1) _cyclesPerRev = 1; + _indexWindow = System.Math.Max(1, _cyclesPerRev / 100); // roughly a 2 ms index pulse + _cyclesSpinUp = (int)(_cpuHz * (_profile.SpinUpMs > 0 ? _profile.SpinUpMs : 1000) / 1000); + } + + public void Clock(int cpuCycles) + { + if (_cyclesPerRev == 0) ConfigureTiming(_cpuHz); + if (!MotorOn) + { + _spinUp = 0; // motor off: spins down (simplified as immediate) + return; + } + if (_spinUp < _cyclesSpinUp) + { + _spinUp += cpuCycles; + return; + } + _rotation += cpuCycles; + while (_rotation >= _cyclesPerRev) _rotation -= _cyclesPerRev; + } + + public void Step(bool towardHigherCylinder) + { + // The physical head can travel a little past the nominal cylinder count - disks are routinely + // over-formatted (e.g. 42 tracks on a nominally-40 3" drive) - so we do not clamp at CylinderCount. + if (towardHigherCylinder) CurrentCylinder++; + else if (CurrentCylinder > 0) CurrentCylinder--; + } + + public void SeekTo(int cylinder) => CurrentCylinder = cylinder < 0 ? 0 : cylinder; + + public MfmTrack CurrentTrack(int side) => Disk?.GetTrack(CurrentCylinder, side); + + public void WriteTrack(int side, MfmTrack track) => Disk?.SetTrack(CurrentCylinder, side, track); + + /// + /// Serialize the mechanical state (head position, motor, rotation). The disk is restored separately. + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("FloppyDrive"); + bool motor = MotorOn; + ser.Sync(nameof(MotorOn), ref motor); + MotorOn = motor; + int cyl = CurrentCylinder; + ser.Sync(nameof(CurrentCylinder), ref cyl); + CurrentCylinder = cyl; + ser.Sync(nameof(_rotation), ref _rotation); + ser.Sync(nameof(_spinUp), ref _spinUp); + ser.EndSection(); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/FluxDisk.cs b/src/BizHawk.Emulation.Cores/Floppy/FluxDisk.cs new file mode 100644 index 00000000000..ac2ccd78db0 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/FluxDisk.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// A whole floppy represented as flux: one MfmTrack per (cylinder, side). Disk-image formats + /// (DSK/EDSK and later HFE/IPF) convert into this. Part of the shared floppy disk subsystem. + /// + public sealed class FluxDisk + { + private readonly Dictionary _tracks = new Dictionary(); + + public int Cylinders { get; private set; } + public int Sides { get; private set; } + public bool WriteProtected { get; set; } + + private static int Key(int cylinder, int side) => (cylinder << 1) | (side & 1); + + public void SetTrack(int cylinder, int side, MfmTrack track) + { + _tracks[Key(cylinder, side)] = track; + if (cylinder + 1 > Cylinders) Cylinders = cylinder + 1; + if (side + 1 > Sides) Sides = side + 1; + } + + /// + /// The flux for a (cylinder, side), or null if that track is not present/formatted. + /// + public MfmTrack GetTrack(int cylinder, int side) + => _tracks.TryGetValue(Key(cylinder, side), out var t) ? t : null; + + /// + /// Extract one side of a double-sided disk as a new single-sided disk (that side's tracks placed at + /// side 0). This is how a double-sided image is split into two selectable disks for the single-headed + /// +3/CPC drive - format-agnostic, since it works on the shared flux representation. + /// + public FluxDisk ExtractSide(int side) + { + var one = new FluxDisk { WriteProtected = WriteProtected }; + for (int cyl = 0; cyl < Cylinders; cyl++) + { + var t = GetTrack(cyl, side); + if (t != null) one.SetTrack(cyl, 0, t); + } + return one; + } + + /// + /// Build a flux disk from a CPC DSK/EDSK image (each track synthesized into MFM cells). + /// + public static FluxDisk FromCpcDsk(byte[] dsk) + { + var parsed = CpcDskConverter.Parse(dsk); + CpcDskConverter.ApplySpeedlockWeakSynthesis(parsed); // reproduce weak sectors a plain DSK omits + var disk = new FluxDisk(); + foreach (var pt in parsed.Tracks) + disk.SetTrack(pt.Cylinder, pt.Side, pt.BuildFlux()); + return disk; + } + + /// + /// Build a flux disk from an HxC HFE image (raw cell bitstream, de-interleaved per side). + /// + public static FluxDisk FromHfe(byte[] hfe) => HfeConverter.ToFluxDisk(hfe); + + /// + /// Build a flux disk from a ZX Spectrum FDI sector image. + /// + public static FluxDisk FromFdi(byte[] fdi) => FdiConverter.ToFluxDisk(fdi); + + /// + /// Build a flux disk from a ZX Spectrum UDI v1.0 track image. + /// + public static FluxDisk FromUdi(byte[] udi) => UdiConverter.ToFluxDisk(udi); + + /// + /// Build a flux disk from a SuperCard Pro (.scp) flux image (flux quantized to MFM cells). + /// + public static FluxDisk FromScp(byte[] scp) => ScpConverter.ToFluxDisk(scp); + + /// + /// Build a flux disk from a headerless raw sector image using an explicit geometry. + /// + public static FluxDisk FromRawSectors(byte[] data, DiskGeometry geometry) => RawSectorConverter.ToFluxDisk(data, geometry); + + /// + /// Build a flux disk from an IPF image (each formatted track rolled into MFM cells). + /// + public static FluxDisk FromIpf(byte[] ipf) + { + var parsed = IpfConverter.Parse(ipf); + var disk = new FluxDisk(); + foreach (var img in parsed.Images) + { + if (!parsed.Data.TryGetValue(img.DataKey, out var data)) continue; + var track = IpfConverter.BuildFluxTrack(img, data); + if (track != null) disk.SetTrack(img.Track, img.Side, track); + } + return disk; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/FluxDiskFormatIdentifier.cs b/src/BizHawk.Emulation.Cores/Floppy/FluxDiskFormatIdentifier.cs new file mode 100644 index 00000000000..526538611b8 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/FluxDiskFormatIdentifier.cs @@ -0,0 +1,109 @@ +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Determines which emulated System a container-agnostic flux disk image (.scp, .hfe) belongs to by + /// decoding it and inspecting the disk contents - the equivalent of DskIdentifier/IpfIdentifier for the + /// flux formats. Those two live in BizHawk.Emulation.Common (which cannot reference this assembly), and + /// SCP/HFE headers do not reliably encode the target machine (a ZX Spectrum and an Amstrad CPC dump both + /// look like "generic MFM"), so the only dependable way to tell them apart is to decode the flux to + /// sectors and apply the same content heuristics DskIdentifier uses. That decode is only available here, + /// so RomLoader (which can reach both assemblies) calls this when the extension leaves the system open. + /// Returns a VSystemID.Raw value, or null when the contents are inconclusive (leave it to the chooser). + /// + public static class FluxDiskFormatIdentifier + { + public static string IdentifySystem(byte[] image) + { + FluxDisk disk; + try + { + if (ScpConverter.IsScp(image)) disk = ScpConverter.ToFluxDisk(image); + else if (HfeConverter.IsHfe(image) || HfeConverter.IsHfeV3(image)) disk = HfeConverter.ToFluxDisk(image); + else return null; + } + catch + { + return null; + } + + return IdentifySystem(disk); + } + + /// + /// Identify the target System from an already-decoded flux disk (exposed for testing and for callers + /// that have a FluxDisk in hand). Returns a VSystemID.Raw value, or null when inconclusive. + /// + public static string IdentifySystem(FluxDisk disk) + { + var t0 = disk?.GetTrack(0, 0); + if (t0 == null) return null; + var secs = StandardMfmFormat.DecodeSectors(t0); + if (secs.Count == 0) return null; // not an IBM/System-34 MFM disk (e.g. Amiga/C64 GCR) - defer + + secs.Sort((a, b) => a.R.CompareTo(b.R)); + + // TR-DOS (Beta 128 / Pentagon): the disk-info sector is track 0 physical sector 9, carrying the + // TR-DOS marker 0x10 at 0xE7 and a disk-type byte 0x16-0x19 at 0xE3. + foreach (var s in secs) + { + if (s.R == 9 && s.Data != null && s.Data.Length > 0xE7 + && s.Data[0xE7] == 0x10 && s.Data[0xE3] >= 0x16 && s.Data[0xE3] <= 0x19) + { + return VSystemID.Raw.ZXSpectrum; + } + } + + // +3DOS: every +3DOS file starts with a 128-byte "PLUS3DOS" header, but the reserved/boot track 0 + // is often blank (the directory and files live on track 1+), so scan the first few cylinders on + // both sides - the same whole-disk search DskIdentifier does for the .dsk container. + int scanCyls = System.Math.Min(disk.Cylinders, 4); + for (int cyl = 0; cyl < scanCyls; cyl++) + { + for (int side = 0; side < disk.Sides; side++) + { + var t = disk.GetTrack(cyl, side); + if (t == null) continue; + foreach (var s in StandardMfmFormat.DecodeSectors(t)) + if (s.Data != null && ContainsAscii(s.Data, "PLUS3DOS")) + return VSystemID.Raw.ZXSpectrum; + } + } + + // Amstrad CPC formats number their sectors from 0x41 (or 0xC1 on side 1 / higher tracks) + byte lowestId = 0xFF; + foreach (var s in secs) if (s.R < lowestId) lowestId = s.R; + if (lowestId is 0x41 or 0xC1) return VSystemID.Raw.AmstradCPC; + + // bootable-record checksum on the first sector: a +3 boot record sums to 3 (mod 256), + // the Amstrad PCW/CPC boot records to 1 or 255 (same test DskIdentifier uses) + var boot = secs[0]; + if (boot.Data != null && boot.Data.Length >= 512) + { + int sum = 0; + for (int i = 0; i < 512; i++) sum += boot.Data[i]; + switch (sum & 0xFF) + { + case 3: return VSystemID.Raw.ZXSpectrum; + case 1: + case 255: return VSystemID.Raw.AmstradCPC; + } + } + + return null; // inconclusive - let the platform chooser decide + } + + private static bool ContainsAscii(byte[] data, string needle) + { + int n = needle.Length; + for (int i = 0; i + n <= data.Length; i++) + { + int j = 0; + while (j < n && data[i + j] == (byte)needle[j]) j++; + if (j == n) return true; + } + return false; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/MfmTrack.cs b/src/BizHawk.Emulation.Cores/Floppy/MfmTrack.cs new file mode 100644 index 00000000000..4a82616704b --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/MfmTrack.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// A single track represented as a stream of MFM cells (bit-packed). This is the canonical + /// "bitstream" the FDC decodes - one cell per bit-time, two cells (clock,data) per encoded data bit. + /// Cell 0 is the first cell after the index. Uniform-density MFM for now; variable per-region cell + /// timing (for IPF Copylock/Speedlock density) is a later layer on top of this. + /// An optional per-cell weak mask marks fuzzy cells (copy protection) that read unpredictably. + /// Part of the shared floppy disk subsystem. + /// + public sealed class MfmTrack + { + private readonly byte[] _cells; // bit-packed, cell i at _cells[i>>3] bit (i&7) + private readonly byte[] _weak; // bit-packed weak-cell mask (null = no weak cells) + public int CellCount { get; } + + public MfmTrack(byte[] packedCells, int cellCount, byte[] weakMask = null) + { + _cells = packedCells; + _weak = weakMask; + CellCount = cellCount; + } + + public bool GetCell(int i) => (_cells[i >> 3] & (1 << (i & 7))) != 0; + + /// + /// True if this cell is weak/fuzzy - it reads unpredictably (the mechanism behind weak-sector copy + /// protection). The reader substitutes a random bit for a weak data cell. + /// + public bool IsWeak(int i) => _weak != null && (_weak[i >> 3] & (1 << (i & 7))) != 0; + + /// + /// Read a 16-cell window starting at pos as a big-endian 16-bit value + /// (cell at pos = bit 15). Used to match sync patterns. Wraps around the track (circular). + /// + public ushort Window16(int pos) + { + ushort v = 0; + for (int i = 0; i < 16; i++) + { + v = (ushort)(v << 1); + if (GetCell((pos + i) % CellCount)) v |= 1; + } + return v; + } + } + + /// + /// Builds an MfmTrack by appending MFM-encoded bytes and the special missing-clock sync + /// marks. Pure cell encoding - CRC is the format builder's concern (it feeds the same bytes to + /// Crc16Ccitt and writes the resulting CRC bytes via WriteByte). Bytes + /// written via WriteByteWeak have their cells flagged weak/fuzzy. + /// + public sealed class MfmTrackWriter + { + // The two IBM System-34 missing-clock sync patterns (16 cells each, MSB = first cell): + public const ushort SyncA1 = 0x4489; // encodes data 0xA1 with a suppressed clock - the ID/DAM sync + public const ushort SyncC2 = 0x5224; // encodes data 0xC2 with a suppressed clock - the IAM sync + + private readonly List _cells = new List(120_000); + private readonly List _weak = new List(120_000); + private int _prevDataBit; // last DATA bit emitted, for the MFM clock rule + private bool _weakMode; // when true, appended cells are flagged weak + + public int CellCount => _cells.Count; + + private void AddCell(bool cell) + { + _cells.Add(cell); + _weak.Add(_weakMode); + } + + private void EmitDataBit(int dataBit) + { + // MFM clock rule: a clock cell is set only between two zero data bits + int clock = (_prevDataBit == 0 && dataBit == 0) ? 1 : 0; + AddCell(clock != 0); + AddCell(dataBit != 0); + _prevDataBit = dataBit; + } + + /// + /// Append one MFM-encoded data byte (MSB first). Does NOT touch any CRC. + /// + public void WriteByte(byte b) + { + for (int i = 7; i >= 0; i--) + EmitDataBit((b >> i) & 1); + } + + /// + /// Append an MFM-encoded byte whose cells are flagged weak/fuzzy (reads vary per pass). + /// + public void WriteByteWeak(byte b) + { + _weakMode = true; + WriteByte(b); + _weakMode = false; + } + + public void WriteBytes(byte value, int count) + { + for (int i = 0; i < count; i++) WriteByte(value); + } + + public void WriteBytes(byte[] data) + { + foreach (var b in data) WriteByte(b); + } + + private void EmitFixed16(ushort pattern) + { + for (int i = 15; i >= 0; i--) + AddCell(((pattern >> i) & 1) != 0); + } + + /// + /// Append an A1 (0x4489) sync mark. Data-bit continuity: A1's last data bit is 1. + /// + public void WriteSyncA1() + { + EmitFixed16(SyncA1); + _prevDataBit = 1; + } + + /// + /// Append a C2 (0x5224) sync mark (IAM). C2's last data bit is 0. + /// + public void WriteSyncC2() + { + EmitFixed16(SyncC2); + _prevDataBit = 0; + } + + /// + /// Append raw cells taken verbatim from sample (MSB first), for stream data that is + /// already at the cell level (IPF Sync/Raw elements, which carry the recorded flux bits directly). + /// + public void WriteRawCells(byte[] sample, int cellCount, bool weak = false) + { + _weakMode = weak; + for (int i = 0; i < cellCount; i++) + { + int idx = i >> 3; + bool bit = idx < sample.Length && ((sample[idx] >> (7 - (i & 7))) & 1) != 0; + AddCell(bit); + _prevDataBit = bit ? 1 : 0; + } + _weakMode = false; + } + + /// + /// Append cellCount weak/fuzzy cells (IPF Fuzzy: consumer-generated bits). + /// + public void WriteWeakCells(int cellCount) + { + _weakMode = true; + for (int i = 0; i < cellCount; i++) AddCell(false); + _weakMode = false; + _prevDataBit = 0; + } + + public MfmTrack Build() + { + int n = _cells.Count; + var packed = new byte[(n + 7) >> 3]; + bool anyWeak = false; + for (int i = 0; i < n; i++) + { + if (_cells[i]) packed[i >> 3] |= (byte)(1 << (i & 7)); + if (_weak[i]) anyWeak = true; + } + + byte[] weak = null; + if (anyWeak) + { + weak = new byte[(n + 7) >> 3]; + for (int i = 0; i < n; i++) + if (_weak[i]) weak[i >> 3] |= (byte)(1 << (i & 7)); + } + + return new MfmTrack(packed, n, weak); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/MfmTrackReader.cs b/src/BizHawk.Emulation.Cores/Floppy/MfmTrackReader.cs new file mode 100644 index 00000000000..458699cb83f --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/MfmTrackReader.cs @@ -0,0 +1,94 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Decodes MFM cells back into bytes and locates address marks - the read side of the flux model, + /// mirroring what the FDC's data separator + shift register do. Operates circularly over the track. + /// + public sealed class MfmTrackReader + { + private readonly MfmTrack _t; + + /// + /// RNG used to resolve weak/fuzzy data cells so repeated reads of a weak sector vary. Seeded and + /// resettable so a savestate/TAS replays identically (the seed lives in serialized state). + /// + public WeakBitRng WeakRng { get; set; } = new WeakBitRng(0); + + public MfmTrackReader(MfmTrack track) => _t = track; + + public int CellCount => _t.CellCount; + + /// + /// Decode the 16-cell window at pos as one data byte (data cells are + /// the odd cells; first data bit is the MSB). A weak data cell yields a random bit. Wraps around. + /// + public byte ReadByteAt(int pos) + { + int b = 0; + for (int k = 0; k < 8; k++) + { + int cell = (pos + 1 + 2 * k) % CellCount; + b <<= 1; + bool bit = _t.IsWeak(cell) ? WeakRng.Next(2) != 0 : _t.GetCell(cell); + if (bit) b |= 1; + } + return (byte)b; + } + + /// + /// From pos, scan forward for an A1 (0x4489) sync, consume consecutive A1 sync + /// windows, and decode the following byte as the address mark. On success pos is + /// left immediately after the mark byte, ready to read the field. Returns false if no A1 is found + /// within one revolution. + /// + public bool TryFindAddressMark(ref int pos, out byte mark, out int a1Count, out int syncStart) + { + mark = 0; + a1Count = 0; + syncStart = -1; + + int scanned = 0; + while (scanned < CellCount && _t.Window16(pos) != MfmTrackWriter.SyncA1) + { + pos = (pos + 1) % CellCount; + scanned++; + } + if (scanned >= CellCount) return false; // no address mark on this track + + syncStart = pos; // cell position of the first A1 sync (identifies this mark uniquely) + + // consume the run of A1 sync marks + while (_t.Window16(pos) == MfmTrackWriter.SyncA1) + { + a1Count++; + pos = (pos + 16) % CellCount; + } + + // the byte after the sync run is the address mark + mark = ReadByteAt(pos); + pos = (pos + 16) % CellCount; + return true; + } + + /// + /// Read count MFM bytes starting at pos, advancing it. + /// + public byte[] ReadBytes(ref int pos, int count) + { + var buf = new byte[count]; + for (int i = 0; i < count; i++) + { + buf[i] = ReadByteAt(pos); + pos = (pos + 16) % CellCount; + } + return buf; + } + + public byte ReadByte(ref int pos) + { + byte b = ReadByteAt(pos); + pos = (pos + 16) % CellCount; + return b; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Floppy/WeakBitRng.cs b/src/BizHawk.Emulation.Cores/Floppy/WeakBitRng.cs new file mode 100644 index 00000000000..92923f62e99 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Floppy/WeakBitRng.cs @@ -0,0 +1,33 @@ +namespace BizHawk.Emulation.Cores.Floppy +{ + /// + /// Deterministic PRNG used to resolve weak/fuzzy cells (copy protection) so repeated reads of a weak sector + /// vary. Unlike System.Random, its whole state is a single value that can be serialized, so + /// weak reads replay identically across savestate/TAS load (the FDC syncs State). splitmix64. + /// + public sealed class WeakBitRng + { + private const ulong Gamma = 0x9E37_79B9_7F4A_7C15UL; + private ulong _state; + + public WeakBitRng(ulong seed = 0) => _state = seed; + + /// + /// The full RNG state - serialize this for deterministic replay. + /// + public ulong State { get => _state; set => _state = value; } + + /// + /// Returns a value in [0, maxExclusive). Mirrors the System.Random.Next(int) signature. + /// + public int Next(int maxExclusive) + { + _state += Gamma; + ulong z = _state; + z = (z ^ (z >> 30)) * 0xBF58_476D_1CE4_E5B9UL; + z = (z ^ (z >> 27)) * 0x94D0_49BB_1331_11EBUL; + z ^= z >> 31; + return maxExclusive <= 1 ? 0 : (int)(z % (ulong)maxExclusive); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Sound/AY391x.cs b/src/BizHawk.Emulation.Cores/Sound/AY391x.cs new file mode 100644 index 00000000000..441b42f103a --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Sound/AY391x.cs @@ -0,0 +1,808 @@ +using System.Collections.Generic; + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Components +{ + /// + /// AY-3-891x PSG sound DSP (shared core). + /// + /// Based heavily on the YM-2149F / AY-3-8910 emulator used in Unreal Speccy + /// (Originally created under Public Domain license by SMT jan.2006) + /// + /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.cpp + /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.h + /// + /// This class contains ONLY the chip DSP and rendering. Host-bus specifics (port + /// address decode, machine cycle timing) live in the platform adapters that wrap it. + /// + public sealed class AY391x : ISoundProvider, IDisposable + { + private int _tStatesPerFrame; + private int _sampleRate; + private int _samplesPerFrame; + private short[] _audioBuffer; + + // Band-limited output. The chip is stepped at base-tick resolution and each change in the summed + // L/R output is fed to a per-channel BlipBuffer as a delta, so tone/noise transitions between output + // samples are captured and band-limited (no aliasing) instead of point-sampled. + private BlipBuffer _blipL; + private BlipBuffer _blipR; + private int _lastL; + private int _lastR; + private int _blipClock; // base-tick position within the current frame + private int _baseTicksPerFrame; // chip base ticks (clock/8) in one host frame + private double _baseTickRate; // chip base-tick rate = ChipClockHz / 8 + + /// + /// Main constructor + /// + public AY391x() + { + } + + /// + /// Initialises the AY chip. chipClockHz is the AY master clock; hostClockHz is the clock that + /// tStatesPerFrame is measured in (e.g. the CPU clock on the Spectrum, the gate-array clock on the + /// CPC), so the base-tick and sample geometry are derived correctly for any machine/clock. + /// + public void Init(int sampleRate, int tStatesPerFrame, int chipClockHz, int hostClockHz) + { + InitTiming(sampleRate, tStatesPerFrame, chipClockHz, hostClockHz); + UpdateVolume(); + Reset(); + } + + /// + /// AY mixer panning configuration + /// + public enum PanConfig + { + MONO = 0, + ABC = 1, + ACB = 2, + BAC = 3, + BCA = 4, + CAB = 5, + CBA = 6, + } + + /// + /// The AY panning configuration + /// + public PanConfig PanningConfiguration + { + get => _currentPanTab; + set + { + if (value != _currentPanTab) + { + _currentPanTab = value; + UpdateVolume(); + } + } + } + + /// + /// The AY chip output volume + /// (0 - 100) + /// + public int Volume + { + get => _volume; + set + { + //value = Math.Max(0, value); + //value = Math.Max(100, value); + if (_volume == value) + { + return; + } + _volume = value; + UpdateVolume(); + } + } + + /// + /// Sets the volume field WITHOUT rebuilding the volume tables. + /// Used to replicate the legacy savestate-load behaviour (restore stored + /// volume without a table rebuild). + /// + public void SetVolumeNoRebuild(int v) => _volume = v; + + /// + /// The master clock frequency of the AY chip (Hz). Drives the base-tick rate (clock/8) and the + /// per-frame timing; set via Init. + /// + public int ChipClockHz { get; set; } = 1773400; + + /// + /// The specific chip in the AY-3-891x / YM2149 family. Governs the number of I/O ports + /// (8910 = A+B, 8912 = A only, 8913 = none), the envelope DAC resolution (YM2149 = 32 levels vs the + /// AY's 16), and register read-back (YM returns the written value; the AY masks undefined bits to 0). + /// Both the ZX Spectrum 128 and the Amstrad CPC use the AY-3-8912. + /// + public enum Variant + { + AY_3_8910, + AY_3_8912, + AY_3_8913, + YM2149, + } + + private Variant _variant = Variant.AY_3_8912; + + public Variant ChipVariant + { + get => _variant; + set + { + if (value == _variant) return; + _variant = value; + if (_volumeTables != null) UpdateVolume(); // envelope DAC resolution depends on the variant + } + } + + /// + /// YM2149 /SEL pin held low: the input clock is internally divided by 2 before the prescaler (lets a + /// YM run from a higher external clock). No effect on the AY parts. Set before Init. + /// + public bool HalfClock { get; set; } + + private bool HasPortA => _variant != Variant.AY_3_8913; + private bool HasPortB => _variant is Variant.AY_3_8910 or Variant.YM2149; + + /// + /// The currently selected register + /// + public int SelectedRegister + { + get => _activeRegister; + set => _activeRegister = (byte)value; + } + + /// + /// Generalized bus alias - latch the active register + /// + public void LatchAddress(int reg) => SelectedRegister = reg & 0x0f; + + /// + /// Generalized bus alias - write to the active register + /// + public void WriteData(int val) => PortWrite(val); + + /// + /// Generalized bus alias - read from the active register + /// + public int ReadData() => PortRead(); + + /// + /// Optional I/O port A input callback (null on platforms that do not use it) + /// + public Func PortAInput; + + /// + /// Optional I/O port B input callback (null on platforms that do not use it) + /// + public Func PortBInput; + + /// + /// Port A direction (true = input) + /// + public bool PortAIsInput = true; + + /// + /// Port B direction (true = input) + /// + public bool PortBIsInput = true; + + /// + /// Used for snapshot generation + /// + public int[] ExportRegisters() + { + return _registers; + } + + /// + /// Resets the PSG + /// + public void Reset() + { + // all registers clear to 0 (datasheet reset) + for (int i = 0; i < 16; i++) + _registers[i] = 0; + _activeRegister = 0; + + // seed the 17-bit noise LFSR (the chip powers up in a non-zero state); clear the generator, + // counter and output state, and recompute the derived state from the cleared registers so the + // chip is in a consistent, hardware-plausible reset (the old build left most of this stale). + _noiseSeed = 0x1FFFF; + _bitA = _bitB = _bitC = _bitN = 0; + _eState = 0; _eDirection = 0; + _countA = _countB = _countC = _countE = _countN = 0; + _lastL = _lastR = 0; + + _dividerA = _dividerB = _dividerC = 0; // tone periods 0 + _dividerE = 0; // envelope period 0 + _dividerN = 0; // noise period 0 + _bit0 = _bit1 = _bit2 = _bit3 = _bit4 = _bit5 = 0; // reg7 = 0 -> all channels enabled (active low) + _eMaskA = _eMaskB = _eMaskC = 0; + _vA = _vB = _vC = 1; // reg8-10 = 0 -> amplitude index 1 -> AYVolumes[1] = 0 (silent) + PortAIsInput = PortBIsInput = true; + } + + /// + /// Reads the value from the currently selected register + /// + public int PortRead() + { + // I/O ports: a present, input-configured port with a host callback returns the pin state. + if (_activeRegister == 14 && HasPortA && PortAInput != null) + { + return PortAIsInput ? PortAInput() : (byte)(PortAInput() & _registers[14]); + } + + if (_activeRegister == 15 && HasPortB && PortBInput != null) + { + return PortBIsInput ? PortBInput() : (byte)(PortBInput() & _registers[15]); + } + + // Register read-back: the YM2149 returns the raw written value; the AY parts read undefined bits + // back as 0 (the stored value is already masked on write). + return _variant == Variant.YM2149 ? _rawWrite[_activeRegister] : _registers[_activeRegister]; + } + + /// + /// Writes to the currently selected register + /// + public void PortWrite(int value) + { + if (_activeRegister >= 0x10) + return; + + _rawWrite[_activeRegister] = value & 0xFF; // kept for YM2149 register read-back (unmasked) + + byte val = (byte)value; + if (_activeRegister is 1 or 3 or 5 or 13) val &= 0x0F; + else if (_activeRegister is 6 or 8 or 9 or 10) val &= 0x1F; + if (_activeRegister != 13 && _registers[_activeRegister] == val) + return; + + _registers[_activeRegister] = val; + + switch (_activeRegister) + { + // Channel A (Combined Pitch) + // (not written to directly) + case 0: + case 1: + _dividerA = _registers[AY_A_FINE] | (_registers[AY_A_COARSE] << 8); + break; + // Channel B (Combined Pitch) + // (not written to directly) + case 2: + case 3: + _dividerB = _registers[AY_B_FINE] | (_registers[AY_B_COARSE] << 8); + break; + // Channel C (Combined Pitch) + // (not written to directly) + case 4: + case 5: + _dividerC = _registers[AY_C_FINE] | (_registers[AY_C_COARSE] << 8); + break; + // Noise Pitch + case 6: + _dividerN = val * 2; + break; + // Mixer + case 7: + _bit0 = 0 - ((val >> 0) & 1); + _bit1 = 0 - ((val >> 1) & 1); + _bit2 = 0 - ((val >> 2) & 1); + _bit3 = 0 - ((val >> 3) & 1); + _bit4 = 0 - ((val >> 4) & 1); + _bit5 = 0 - ((val >> 5) & 1); + PortAIsInput = (val & 0x40) == 0; + PortBIsInput = (val & 0x80) == 0; + break; + // Channel Volumes + case 8: + _eMaskA = (val & 0x10) != 0 ? -1 : 0; + _vA = ((val & 0x0F) * 2 + 1) & ~_eMaskA; + break; + case 9: + _eMaskB = (val & 0x10) != 0 ? -1 : 0; + _vB = ((val & 0x0F) * 2 + 1) & ~_eMaskB; + break; + case 10: + _eMaskC = (val & 0x10) != 0 ? -1 : 0; + _vC = ((val & 0x0F) * 2 + 1) & ~_eMaskC; + break; + // Envelope (Combined Duration) + // (not written to directly) + case 11: + case 12: + _dividerE = _registers[AY_E_FINE] | (_registers[AY_E_COARSE] << 8); + break; + // Envelope Shape + case 13: + // reset the envelope counter + _countE = 0; + + if ((_registers[AY_E_SHAPE] & 4) != 0) + { + // attack + _eState = 0; + _eDirection = 1; + } + else + { + // decay + _eState = 31; + _eDirection = -1; + } + break; + case 14: + // IO Port - not implemented + break; + } + } + + /// + /// Start of frame + /// + public void StartFrame() + { + _blipClock = 0; + BufferUpdate(0); + } + + /// + /// End of frame + /// + public void EndFrame() + { + BufferUpdate(_tStatesPerFrame); + } + + /// + /// Updates the audiobuffer based on the current frame t-state + /// + public void UpdateSound(int frameCycle) + { + BufferUpdate(frameCycle); + } + + /// + /// Register indicies + /// + private const int AY_A_FINE = 0; + private const int AY_A_COARSE = 1; + private const int AY_B_FINE = 2; + private const int AY_B_COARSE = 3; + private const int AY_C_FINE = 4; + private const int AY_C_COARSE = 5; + private const int AY_NOISEPITCH = 6; + private const int AY_MIXER = 7; + private const int AY_A_VOL = 8; + private const int AY_B_VOL = 9; + private const int AY_C_VOL = 10; + private const int AY_E_FINE = 11; + private const int AY_E_COARSE = 12; + private const int AY_E_SHAPE = 13; + private const int AY_PORT_A = 14; + private const int AY_PORT_B = 15; + + /// + /// The register array + /// + /* + The AY-3-8910/8912 contains 16 internal registers as follows: + + Register Function Range + 0 Channel A fine pitch 8-bit (0-255) + 1 Channel A course pitch 4-bit (0-15) + 2 Channel B fine pitch 8-bit (0-255) + 3 Channel B course pitch 4-bit (0-15) + 4 Channel C fine pitch 8-bit (0-255) + 5 Channel C course pitch 4-bit (0-15) + 6 Noise pitch 5-bit (0-31) + 7 Mixer 8-bit (see below) + 8 Channel A volume 4-bit (0-15, see below) + 9 Channel B volume 4-bit (0-15, see below) + 10 Channel C volume 4-bit (0-15, see below) + 11 Envelope fine duration 8-bit (0-255) + 12 Envelope course duration 8-bit (0-255) + 13 Envelope shape 4-bit (0-15) + 14 I/O port A 8-bit (0-255) + 15 I/O port B 8-bit (0-255) (Not present on the AY-3-8912) + + * The volume registers (8, 9 and 10) contain a 4-bit setting but if bit 5 is set then that channel uses the + envelope defined by register 13 and ignores its volume setting. + * The mixer (register 7) is made up of the following bits (low=enabled): + + Bit: 7 6 5 4 3 2 1 0 + Register: I/O I/O Noise Noise Noise Tone Tone Tone + Channel: B A C B A C B A + + The AY-3-8912 ignores bit 7 of this register. + */ + private int[] _registers = new int[16]; + + /// + /// The last raw (unmasked) byte written to each register, used only for YM2149 register read-back. + /// + private int[] _rawWrite = new int[16]; + + /// + /// The currently selected register + /// + private byte _activeRegister; + + /// + /// Channel generator state + /// + private int _bitA; + private int _bitB; + private int _bitC; + + /// + /// Envelope state + /// + private int _eState; + + /// + /// Envelope direction + /// + private int _eDirection; + + /// + /// Noise seed + /// + private int _noiseSeed; + + /// + /// Mixer state + /// + private int _bit0; + private int _bit1; + private int _bit2; + private int _bit3; + private int _bit4; + private int _bit5; + + /// + /// Noise generator state + /// + private int _bitN; + + /// + /// Envelope masks + /// + private int _eMaskA; + private int _eMaskB; + private int _eMaskC; + + /// + /// Amplitudes + /// + private int _vA; + private int _vB; + private int _vC; + + /// + /// Channel gen counters + /// + private int _countA; + private int _countB; + private int _countC; + + /// + /// Envelope gen counter + /// + private int _countE; + + /// + /// Noise gen counter + /// + private int _countN; + + /// + /// Channel gen dividers + /// + private int _dividerA; + private int _dividerB; + private int _dividerC; + + /// + /// Envelope gen divider + /// + private int _dividerE; + + /// + /// Noise gen divider + /// + private int _dividerN; + + /// + /// Panning table list + /// + private static readonly List PanTabs = new List + { + // MONO + new uint[] { 50,50, 50,50, 50,50 }, + // ABC + new uint[] { 100,10, 66,66, 10,100 }, + // ACB + new uint[] { 100,10, 10,100, 66,66 }, + // BAC + new uint[] { 66,66, 100,10, 10,100 }, + // BCA + new uint[] { 10,100, 100,10, 66,66 }, + // CAB + new uint[] { 66,66, 10,100, 100,10 }, + // CBA + new uint[] { 10,100, 66,66, 100,10 } + }; + + /// + /// The currently selected panning configuration + /// + private PanConfig _currentPanTab = PanConfig.ABC; + + /// + /// The current volume + /// + private int _volume = 75; + + /// + /// Volume tables state + /// + private uint[][] _volumeTables; + + /// + /// Volume table to be used + /// + private static readonly uint[] AYVolumes = + { + 0x0000,0x0000,0x0340,0x0340,0x04C0,0x04C0,0x06F2,0x06F2, + 0x0A44,0x0A44,0x0F13,0x0F13,0x1510,0x1510,0x227E,0x227E, + 0x289F,0x289F,0x414E,0x414E,0x5B21,0x5B21,0x7258,0x7258, + 0x905E,0x905E,0xB550,0xB550,0xD7A0,0xD7A0,0xFFFF,0xFFFF, + }; + + /// + /// The YM2149's 32-level (5-bit) envelope DAC, ~1.5 dB per step (the AY-3-8910 uses the 16-level table + /// above, i.e. every other step, ~3 dB). Built from the logarithmic model (full scale = 0xFFFF); a + /// 4-bit fixed volume still lands on the odd indices, so it stays 16-level as on hardware. + /// + private static readonly uint[] YMVolumes = BuildYmVolumes(); + + private static uint[] BuildYmVolumes() + { + var t = new uint[32]; + for (int n = 1; n < 32; n++) + t[n] = (uint)System.Math.Round(65535.0 * System.Math.Pow(10.0, -1.5 * (31 - n) / 20.0)); + return t; // t[0] = 0 (silent) + } + + /// + /// Forces an update of the volume tables + /// + private void UpdateVolume() + { + int upperFloor = 40000; + var inc = (0xFFFF - upperFloor) / 100; + + var vol = inc * _volume; // ((ulong)0xFFFF * (ulong)_volume / 100UL) - 20000 ; + var amps = _variant == Variant.YM2149 ? YMVolumes : AYVolumes; + _volumeTables = new uint[6][]; + + // parent array + for (int j = 0; j < _volumeTables.Length; j++) + { + _volumeTables[j] = new uint[32]; + + // child array + for (int i = 0; i < _volumeTables[j].Length; i++) + { + _volumeTables[j][i] = (uint)( + (PanTabs[(int)_currentPanTab][j] * amps[i] * vol) / + (3 * 65535 * 100)); + } + } + } + + /// + /// Initializes timing information for the frame + /// + private void InitTiming(int sampleRate, int frameTactCount, int chipClockHz, int hostClockHz) + { + _sampleRate = sampleRate; + _tStatesPerFrame = frameTactCount; + ChipClockHz = chipClockHz; + + // The AY tone/noise generators run at the chip base-tick rate = clock/8 (the /16 prescaler with a + // toggling output). Derive the per-frame base-tick and output-sample counts from the real frame + // duration (frameTactCount / hostClockHz) so pitch and speed are correct for any clock/machine. + double frameSeconds = frameTactCount / (double)hostClockHz; + // YM2149 /SEL low divides the input clock by 2 before the /8 base-tick prescaler. + _baseTickRate = chipClockHz / (HalfClock ? 16.0 : 8.0); + _baseTicksPerFrame = (int)System.Math.Round(_baseTickRate * frameSeconds); + _samplesPerFrame = (int)System.Math.Round(sampleRate * frameSeconds); + + int cap = _samplesPerFrame + 64; + _audioBuffer = new short[cap * 2]; + + _blipL?.Dispose(); + _blipR?.Dispose(); + _blipL = new BlipBuffer(cap); + _blipR = new BlipBuffer(cap); + _blipL.SetRates(_baseTickRate, _sampleRate); + _blipR.SetRates(_baseTickRate, _sampleRate); + _lastL = _lastR = 0; + _blipClock = 0; + } + + /// + /// Updates the audiobuffer based on the current frame t-state + /// + private void BufferUpdate(int cycle) + { + if (cycle > _tStatesPerFrame) + cycle = _tStatesPerFrame; + + int targetTick = (_baseTicksPerFrame * cycle) / _tStatesPerFrame; + + while (_blipClock < targetTick) + { + if (++_countA >= _dividerA) { _countA = 0; _bitA ^= -1; } + if (++_countB >= _dividerB) { _countB = 0; _bitB ^= -1; } + if (++_countC >= _dividerC) { _countC = 0; _bitC ^= -1; } + if (++_countN >= _dividerN) + { + _countN = 0; + _noiseSeed = (_noiseSeed * 2 + 1) ^ (((_noiseSeed >> 16) ^ (_noiseSeed >> 13)) & 1); + _bitN = 0 - ((_noiseSeed >> 16) & 1); + } + if (++_countE >= _dividerE) + { + _countE = 0; + _eState += _eDirection; + if ((_eState & ~31) != 0) + { + var sh = _registers[AY_E_SHAPE]; + if (sh is <= 7 or 9 or 15) _eState = _eDirection = 0; + else if (sh is 8 or 12) _eState &= 31; + else if (sh is 10 or 14) { _eDirection = -_eDirection; _eState += _eDirection; } + else { _eState = 31; _eDirection = 0; } + } + } + + var mixA = ((_eMaskA & _eState) | _vA) & ((_bitA | _bit0) & (_bitN | _bit3)); + var mixB = ((_eMaskB & _eState) | _vB) & ((_bitB | _bit1) & (_bitN | _bit4)); + var mixC = ((_eMaskC & _eState) | _vC) & ((_bitC | _bit2) & (_bitN | _bit5)); + + int l = (int)(_volumeTables[0][mixA] + _volumeTables[2][mixB] + _volumeTables[4][mixC]); + int r = (int)(_volumeTables[1][mixA] + _volumeTables[3][mixB] + _volumeTables[5][mixC]); + + if (l != _lastL) { _blipL.AddDelta((uint)_blipClock, l - _lastL); _lastL = l; } + if (r != _lastR) { _blipR.AddDelta((uint)_blipClock, r - _lastR); _lastR = r; } + + _blipClock++; + } + } + + public bool CanProvideAsync => false; + + public SyncSoundMode SyncMode => SyncSoundMode.Sync; + + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Sync) + throw new InvalidOperationException("Only Sync mode is supported."); + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + public void DiscardSamples() + { + _blipL.EndFrame((uint)_blipClock); + _blipR.EndFrame((uint)_blipClock); + _blipL.Clear(); + _blipR.Clear(); + _blipClock = 0; + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + _blipL.EndFrame((uint)_blipClock); + _blipR.EndFrame((uint)_blipClock); + int avail = _blipL.SamplesAvailable(); + _blipL.ReadSamplesLeft(_audioBuffer, avail); + _blipR.ReadSamplesRight(_audioBuffer, avail); + // return the actual clock-derived count; the host mixer resamples each source to its target + nsamp = avail; + samples = _audioBuffer; + _blipClock = 0; + } + + public int nullDump = 0; + + /// + /// Releases the unmanaged BlipBuffer instances + /// + public void Dispose() + { + _blipL?.Dispose(); + _blipR?.Dispose(); + _blipL = null; + _blipR = null; + } + + /// + /// State serialization + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("PSG-AY"); + + ser.Sync(nameof(_tStatesPerFrame), ref _tStatesPerFrame); + ser.Sync(nameof(_sampleRate), ref _sampleRate); + ser.Sync(nameof(_samplesPerFrame), ref _samplesPerFrame); + + ser.Sync(nameof(_lastL), ref _lastL); + ser.Sync(nameof(_lastR), ref _lastR); + ser.Sync(nameof(_blipClock), ref _blipClock); + + ser.Sync(nameof(_registers), ref _registers, false); + ser.Sync(nameof(_rawWrite), ref _rawWrite, false); + ser.Sync(nameof(_activeRegister), ref _activeRegister); + ser.Sync(nameof(_bitA), ref _bitA); + ser.Sync(nameof(_bitB), ref _bitB); + ser.Sync(nameof(_bitC), ref _bitC); + ser.Sync(nameof(_eState), ref _eState); + ser.Sync(nameof(_eDirection), ref _eDirection); + ser.Sync(nameof(_noiseSeed), ref _noiseSeed); + ser.Sync(nameof(_bit0), ref _bit0); + ser.Sync(nameof(_bit1), ref _bit1); + ser.Sync(nameof(_bit2), ref _bit2); + ser.Sync(nameof(_bit3), ref _bit3); + ser.Sync(nameof(_bit4), ref _bit4); + ser.Sync(nameof(_bit5), ref _bit5); + ser.Sync(nameof(PortAIsInput), ref PortAIsInput); + ser.Sync(nameof(PortBIsInput), ref PortBIsInput); + ser.Sync(nameof(_bitN), ref _bitN); + ser.Sync(nameof(_eMaskA), ref _eMaskA); + ser.Sync(nameof(_eMaskB), ref _eMaskB); + ser.Sync(nameof(_eMaskC), ref _eMaskC); + ser.Sync(nameof(_vA), ref _vA); + ser.Sync(nameof(_vB), ref _vB); + ser.Sync(nameof(_vC), ref _vC); + ser.Sync(nameof(_countA), ref _countA); + ser.Sync(nameof(_countB), ref _countB); + ser.Sync(nameof(_countC), ref _countC); + ser.Sync(nameof(_countE), ref _countE); + ser.Sync(nameof(_countN), ref _countN); + ser.Sync(nameof(_dividerA), ref _dividerA); + ser.Sync(nameof(_dividerB), ref _dividerB); + ser.Sync(nameof(_dividerC), ref _dividerC); + ser.Sync(nameof(_dividerE), ref _dividerE); + ser.Sync(nameof(_dividerN), ref _dividerN); + ser.SyncEnum(nameof(_currentPanTab), ref _currentPanTab); + ser.Sync(nameof(_volume), ref nullDump); + + for (int i = 0; i < 6; i++) + { + ser.Sync("volTable" + i, ref _volumeTables[i], false); + } + + ser.EndSection(); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Tapes/ITapeHost.cs b/src/BizHawk.Emulation.Cores/Tapes/ITapeHost.cs new file mode 100644 index 00000000000..f9aec630944 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Tapes/ITapeHost.cs @@ -0,0 +1,48 @@ +namespace BizHawk.Emulation.Cores.Tapes +{ + /// + /// The per-core services a TapeDeck needs from its host machine. This lets the shared deck + /// generate the tape signal and drive transport without referencing any core-specific CPU or machine type: + /// the host supplies the CPU cycle counter, feeds its own tape beeper, and receives tape notifications. + /// A core with a tape motor/remote line or a fast-loader layers those on top; the deck itself is unaware. + /// + public interface ITapeHost + { + /// + /// Running total of executed CPU cycles. The deck compares pulse periods against this. + /// + long TotalExecutedCycles { get; } + + /// + /// True when the host is running in 48K mode (gates the STOP_THE_TAPE_48K command block). + /// + bool IsIn48kMode { get; } + + /// + /// True when non-cycle-accurate fast/trap tape loading is permitted (i.e. emulation is not + /// deterministic). Reserved for a future speed-loader; the cycle-accurate signal path does not use it. + /// + bool FastLoadAllowed { get; } + + /// + /// Feeds the current tape EAR level to the host's tape beeper. + /// + void FeedBeeper(bool earLevel); + + // Notifications - a host may surface these on-screen (OSD) or ignore them entirely. + void NotifyPlay(); + void NotifyStop(); + void NotifyRewind(); + void NotifyNextBlock(string blockInfo); + void NotifyPrevBlock(string blockInfo); + void NotifyPlayingBlock(string blockInfo); + void NotifySkipBlock(string blockInfo); + void NotifyStoppedAuto(); + + /// + /// A STOP_THE_TAPE command block was reached during playback. A host that auto-detects tape activity + /// may use this to reset its auto-stop timing; others can ignore it. + /// + void NotifyStopCommand(); + } +} diff --git a/src/BizHawk.Emulation.Cores/Tapes/TapeCommand.cs b/src/BizHawk.Emulation.Cores/Tapes/TapeCommand.cs new file mode 100644 index 00000000000..c325f59e5a2 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Tapes/TapeCommand.cs @@ -0,0 +1,24 @@ +namespace BizHawk.Emulation.Cores.Tapes +{ + /// + /// Represents the possible commands that can be raised from each tape block + /// + public enum TapeCommand + { + NONE, + STOP_THE_TAPE, + STOP_THE_TAPE_48K, + BEGIN_GROUP, + END_GROUP, + SHOW_MESSAGE, + + // control-flow markers, consumed while a converter expands the tape into its final linear block list + // (they do not appear in the played list, so the tape player never has to interpret them) + JUMP, + LOOP_START, + LOOP_END, + CALL_SEQUENCE, + RETURN_FROM_SEQUENCE, + SELECT_BLOCK, + } +} diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs b/src/BizHawk.Emulation.Cores/Tapes/TapeDataBlock.cs similarity index 81% rename from src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs rename to src/BizHawk.Emulation.Cores/Tapes/TapeDataBlock.cs index 293732f6c38..dfe178ae6f0 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs +++ b/src/BizHawk.Emulation.Cores/Tapes/TapeDataBlock.cs @@ -1,11 +1,18 @@ -using BizHawk.Common; using System.Collections.Generic; using System.Linq; -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Tapes { /// - /// Represents a tape block + /// Represents a tape block. + /// This is the shared, core-agnostic tape data model used by any core with a standard tape player + /// (originally the ZX Spectrum datacorder; the same block structure covers the Amstrad CPC CDT format). + /// Pulse timings in DataPeriods are stored in the format's native 3.5MHz T-state reference; + /// a consuming core scales them to its own CPU clock when generating the signal. + /// BlockData retains the raw decoded payload so a future trap/flash-loader can read block + /// bytes directly rather than replaying pulses. /// public class TapeDataBlock { @@ -79,6 +86,18 @@ public void AddMetaData(BlockDescriptorTitle descriptor, string data) public bool InitialPulseLevel; + /// + /// Control-flow parameter for a control block (jump: signed relative block offset; loop start: + /// repetition count). Used only while a converter expands the tape; control blocks are consumed during + /// expansion and never appear in the final played list, so this is not serialized. + /// + public int ControlValue; + + /// + /// Control-flow relative block offsets for a call-sequence or select block (see ControlValue). + /// + public int[] ControlOffsets; + /// /// Command that is raised by this data block /// (that may or may not need to be acted on) @@ -206,8 +225,7 @@ public enum BlockType // zxhawk proprietry PAUSE_BLOCK, - - WAV_Recording + WAV_Recording, } @@ -251,6 +269,6 @@ public enum BlockDescriptorTitle Origin, Comments, - Needs_Parsing + Needs_Parsing, } } diff --git a/src/BizHawk.Emulation.Cores/Tapes/TapeDeck.cs b/src/BizHawk.Emulation.Cores/Tapes/TapeDeck.cs new file mode 100644 index 00000000000..7b23bb001e1 --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Tapes/TapeDeck.cs @@ -0,0 +1,483 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Tapes +{ + /// + /// + /// A shared, core-agnostic tape player (datacorder). Holds the loaded tape as a list of + /// TapeDataBlock, drives transport (play / stop / rewind / block skip), and generates the + /// EAR signal by comparing pulse periods against the host CPU's cycle counter. + /// + /// + /// Tape formats store pulse timings in a 3.5MHz T-state reference. cyclesPerTapeTState scales those into + /// the host CPU's clock domain: 1.0 for a 3.5MHz machine (a plain move, no change), a larger ratio for a + /// faster clock (e.g. the Amstrad CPC at 4.0/3.5). At exactly 1.0 no scaling is applied, so behaviour is + /// bit-for-bit identical to comparing raw periods to cycles. + /// + /// + /// Everything core-specific (auto-detect of tape reads, trap/fast loading, port I/O, a motor/remote line) + /// lives in the owning core behind ITapeHost; this class references no CPU or machine type. + /// + /// + public sealed class TapeDeck + { + private readonly ITapeHost _host; + + /// + /// Host CPU cycles per 3.5MHz tape T-state. 1.0 leaves periods untouched (a 3.5MHz host); other values + /// scale the format's 3.5MHz-referenced timings into the host clock domain. + /// + private readonly double _cyclesPerTapeTState; + + public TapeDeck(ITapeHost host, double cyclesPerTapeTState) + { + _host = host; + _cyclesPerTapeTState = cyclesPerTapeTState; + } + + /// + /// Scales a raw (3.5MHz-referenced) pulse period into host CPU cycles. A ratio of exactly 1.0 returns + /// the period unchanged, keeping a 3.5MHz host bit-identical to the pre-scaling behaviour. + /// + private int ScalePeriod(int period) + => _cyclesPerTapeTState == 1.0 ? period : (int)(period * _cyclesPerTapeTState); + + /// + /// Internal counter used to trigger tape buzzer output + /// + private int counter = 0; + + /// + /// The index of the current tape data block that is loaded + /// + private int _currentDataBlockIndex = 0; + public int CurrentDataBlockIndex + { + get + { + if (_dataBlocks.Count > 0) { return _currentDataBlockIndex; } + else { return -1; } + } + set + { + if (value == _currentDataBlockIndex) { return; } + if (value < _dataBlocks.Count && value >= 0) + { + _currentDataBlockIndex = value; + _position = 0; + } + } + } + + /// + /// The current position within the current data block + /// + private int _position = 0; + public int Position + { + get + { + if (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) { return 0; } + + return _position; + } + } + + /// + /// Signs whether the tape is currently playing or not + /// + private bool _tapeIsPlaying = false; + public bool TapeIsPlaying => _tapeIsPlaying; + + /// + /// A list of the currently loaded data blocks + /// + private List _dataBlocks = new List(); + public List DataBlocks + { + get => _dataBlocks; + set => _dataBlocks = value; + } + + /// + /// Stores the last CPU t-state value + /// + private long _lastCycle = 0; + + /// + /// Edge + /// + private int _waitEdge = 0; + + /// + /// Current tapebit state + /// + private bool currentState = false; + + /// + /// The current EAR level presented by the tape (read by the host's tape input port). + /// + public bool CurrentEarLevel + { + get => currentState; + set => currentState = value; + } + + /// + /// Builds the human-readable "(n of m) : description - key: value" string for a block, used in the + /// tape notifications. + /// + private string BlockInfo(int index) + { + var bl = _dataBlocks[index]; + StringBuilder sbd = new StringBuilder(); + sbd.Append('('); + sbd.Append((index + 1) + " of " + _dataBlocks.Count); + sbd.Append(") : "); + sbd.Append(bl.BlockDescription); + if (bl.MetaData.Count > 0) + { + sbd.Append(" - "); + sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); + } + return sbd.ToString(); + } + + /// + /// Starts the tape playing from the beginning of the current block + /// + public void Play() + { + if (_tapeIsPlaying) + return; + + _host.NotifyPlay(); + + // update the lastCycle + _lastCycle = _host.TotalExecutedCycles; + + // reset waitEdge and position + _waitEdge = 0; + _position = 0; + + if (_dataBlocks.Count > 0 && _currentDataBlockIndex >= 0) + { + while (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) + { + // we are at the end of a data block - move to the next + _position = 0; + _currentDataBlockIndex++; + + // are we at the end of the tape? + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + break; + } + } + + // check for end of tape + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + // end of tape reached. Rewind to beginning. (The original also called AutoStopTape here, + // but the tape is not yet playing at this point so that was a no-op.) + RTZ(); + return; + } + + // update waitEdge with the current position in the current block + _waitEdge = ScalePeriod(_dataBlocks[_currentDataBlockIndex].DataPeriods[_position]); + + // sign that the tape is now playing + _tapeIsPlaying = true; + } + } + + /// + /// Stops the tape + /// (should move to the beginning of the next block) + /// + public void Stop() + { + if (!_tapeIsPlaying) + return; + + _host.NotifyStop(); + + // sign that the tape is no longer playing + _tapeIsPlaying = false; + + if (_currentDataBlockIndex >= 0 + && _position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count - 1) // the block is still playing back + { + // move to the next block + _currentDataBlockIndex++; + + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + _currentDataBlockIndex = -1; + } + + // reset waitEdge and position + _waitEdge = 0; + _position = 0; + + if (_currentDataBlockIndex < 0 && _dataBlocks.Count > 0) + { + // move the index on to 0 + _currentDataBlockIndex = 0; + } + } + + // update the lastCycle + _lastCycle = _host.TotalExecutedCycles; + } + + /// + /// Rewinds the tape to it's beginning (return to zero) + /// + public void RTZ() + { + Stop(); + _host.NotifyRewind(); + _currentDataBlockIndex = 0; + } + + /// + /// Performs a block skip operation on the current tape + /// TRUE: skip forward + /// FALSE: skip backward + /// + public void SkipBlock(bool skipForward) + { + int blockCount = _dataBlocks.Count; + int targetBlockId = _currentDataBlockIndex; + + if (skipForward) + { + if (_currentDataBlockIndex == blockCount - 1) + { + // last block, go back to beginning + targetBlockId = 0; + } + else + { + targetBlockId++; + } + } + else + { + if (_currentDataBlockIndex == 0) + { + // already first block, goto last block + targetBlockId = blockCount - 1; + } + else + { + targetBlockId--; + } + } + + string info = BlockInfo(targetBlockId); + + if (skipForward) + _host.NotifyNextBlock(info); + else + _host.NotifyPrevBlock(info); + + CurrentDataBlockIndex = targetBlockId; + } + + /// + /// Resets the tape + /// + public void Reset() + { + RTZ(); + } + + /// + /// Is called every cpu cycle but runs every so many t-states. + /// This enables the tape device to play out even if the host is not requesting tape data. + /// + public void TapeCycle() + { + if (_tapeIsPlaying) + { + counter++; + + if (counter > 20) + { + counter = 0; + bool state = GetEarBit(_host.TotalExecutedCycles); + _host.FeedBeeper(state); + } + } + } + + /// + /// Simulates the 'EAR' input reading data from the tape + /// + public bool GetEarBit(long cpuCycle) + { + // decide how many cycles worth of data we are capturing + long cycles = cpuCycle - _lastCycle; + + bool is48k = _host.IsIn48kMode; + + // check whether tape is actually playing + if (!_tapeIsPlaying) + { + // it's not playing. Update lastCycle and return + _lastCycle = cpuCycle; + return false; + } + + // check for end of tape + if (_currentDataBlockIndex < 0) + { + // end of tape reached - RTZ (and stop) + RTZ(); + return currentState; + } + + // process the cycles based on the waitEdge + while (cycles >= _waitEdge) + { + // decrement cycles + cycles -= _waitEdge; + + if (_position == 0 && _tapeIsPlaying) + { + // notify about the current block + _host.NotifyPlayingBlock(BlockInfo(_currentDataBlockIndex)); + } + + // increment the current period position + _position++; + + if (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) + { + // we have reached the end of the current block + if (_dataBlocks[_currentDataBlockIndex].DataPeriods.Count == 0) + { + // notify about the current block (we are skipping it because its empty) + _host.NotifySkipBlock(BlockInfo(_currentDataBlockIndex)); + } + + // skip any empty blocks (and process any command blocks) + while (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) + { + // check for any commands + var command = _dataBlocks[_currentDataBlockIndex].Command; + var block = _dataBlocks[_currentDataBlockIndex]; + bool shouldStop = false; + switch (command) + { + // Stop the tape command found - if this is the end of the tape RTZ + // otherwise just STOP and move to the next block + case TapeCommand.STOP_THE_TAPE: + + _host.NotifyStoppedAuto(); + shouldStop = true; + + if (_currentDataBlockIndex >= _dataBlocks.Count) + RTZ(); + else + { + Stop(); + } + + _host.NotifyStopCommand(); + break; + case TapeCommand.STOP_THE_TAPE_48K: + if (is48k) + { + _host.NotifyStoppedAuto(); + shouldStop = true; + + if (_currentDataBlockIndex >= _dataBlocks.Count) + RTZ(); + else + { + Stop(); + } + + _host.NotifyStopCommand(); + } + break; + } + + if (shouldStop) + break; + + _position = 0; + _currentDataBlockIndex++; + + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + break; + } + } + + // check for end of tape + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + _currentDataBlockIndex = -1; + RTZ(); + return currentState; + } + } + + // update waitEdge with current position within the current block + var curBlock = _dataBlocks[_currentDataBlockIndex]; + _waitEdge = _position < curBlock.DataPeriods.Count + ? ScalePeriod(curBlock.DataPeriods[_position]) : 0; + + // Set the current EAR state. Blocks that carry an explicit per-period level (e.g. direct + // recording / CSW / standard-speed data) use it; blocks that only populate DataPeriods + // (pure tone, pulse sequences, turbo pilots - as used by Speedlock and other custom loaders) + // toggle the level each period, which is the canonical datacorder behaviour. Guarding against + // DataLevels being shorter than DataPeriods here also stops a malformed/short block from + // throwing an IndexOutOfRangeException that would crash the whole application. + if (_position < curBlock.DataLevels.Count) + currentState = curBlock.DataLevels[_position]; + else + currentState = !currentState; + } + + // update lastCycle and return currentstate + _lastCycle = cpuCycle - cycles; + + return currentState; + } + + /// + /// Resets the deck's activity timestamp to the host's current cycle (used by a host that auto-stops + /// the tape when the CPU has not read from it for a while). + /// + public void ResetLastCycleToNow() + { + _lastCycle = _host.TotalExecutedCycles; + } + + /// + /// Bizhawk state serialization for the transport/signal state. Field names and order match the pre- + /// extraction datacorder so existing savestate layout is preserved (the owning device serializes its + /// own auto-detect fields immediately after, within the same section). + /// + public void SyncState(Serializer ser) + { + ser.Sync(nameof(counter), ref counter); + ser.Sync(nameof(_currentDataBlockIndex), ref _currentDataBlockIndex); + ser.Sync(nameof(_position), ref _position); + ser.Sync(nameof(_tapeIsPlaying), ref _tapeIsPlaying); + ser.Sync(nameof(_lastCycle), ref _lastCycle); + ser.Sync(nameof(_waitEdge), ref _waitEdge); + ser.Sync(nameof(currentState), ref currentState); + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Tapes/TapeProtection.cs b/src/BizHawk.Emulation.Cores/Tapes/TapeProtection.cs new file mode 100644 index 00000000000..137b28cf7cc --- /dev/null +++ b/src/BizHawk.Emulation.Cores/Tapes/TapeProtection.cs @@ -0,0 +1,317 @@ +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Tapes +{ + /// + /// A recognized ZX Spectrum tape loading-scheme / copy-protection (None if nothing matched). + /// + public enum TapeProtectionScheme + { + None, + StandardRom, + Speedlock, + Alkatraz, + Bleepload, + DigitalIntegration, + SearchLoader, + PaulOwens, + Dinaload, + Ftl, + Zydroload, + Novaload, + Gremlin, + Gremlin2, + Micromega, + TheEdge, + SoftwareProjects, + Microprose, + Sentient, + RollerCoaster, + Haxpoc, + Rqfl, + Edos, + Moonlighter, + ZetaLoad, + Players, + PowerLoad, + EliteUniLoader, + SoftLock, + } + + /// + /// Identifies well-known ZX Spectrum tape loading / copy-protection schemes from the parsed tape blocks, + /// for logging (the tape analogue of DiskProtection). + /// Detection is PASSIVE - report-only; it never changes loading behaviour, so it runs unconditionally + /// (no determinism gate). Two signal sources, mirroring the disk detector: + /// (1) CODE-side - most custom loaders deliver their edge-poll stub as ordinary block data, so the loop's + /// opcode bytes around IN A,(0xFE) are present in TapeDataBlock.BlockData. The + /// bytes around the port read - the counter (INC B/DEC B), the bit mask (0x20 = bit 5 vs + /// 0x40 = bit 6) and the no-edge exit (RET NC / RET Z / NOP / JP) - are what + /// distinguish one scheme's ROM-derived loop from another's. + /// (2) PULSE-side - a few schemes have a uniquely distinctive pilot-tone pulse width (Gremlin 2 = 2670T, + /// Micromega = 1739T) that identifies them straight from the pulse list. + /// Signatures follow the documented loader fingerprints (community references / Fuse's documented opcode + /// tables read as facts); this is an independent implementation of those facts, not copied code. + /// + public static class TapeProtection + { + public static string DisplayName(TapeProtectionScheme scheme) + => scheme switch + { + TapeProtectionScheme.None => "None (or unknown)", + TapeProtectionScheme.StandardRom => "Standard ROM loader", + TapeProtectionScheme.DigitalIntegration => "Digital Integration", + TapeProtectionScheme.SearchLoader => "Search Loader (MultiLoad)", + TapeProtectionScheme.PaulOwens => "Paul Owens Protection System", + TapeProtectionScheme.Ftl => "FTL / Gargoyle", + TapeProtectionScheme.Gremlin => "Gremlin 1", + TapeProtectionScheme.Gremlin2 => "Gremlin 2", + TapeProtectionScheme.TheEdge => "The Edge", + TapeProtectionScheme.SoftwareProjects => "Software Projects", + TapeProtectionScheme.RollerCoaster => "Roller Coaster", + TapeProtectionScheme.Haxpoc => "Haxpoc-Lock", + TapeProtectionScheme.Rqfl => "Really Quite Fast Loader (RQFL)", + TapeProtectionScheme.Edos => "EDOS", + TapeProtectionScheme.ZetaLoad => "ZetaLoad (RAMSOFT)", + TapeProtectionScheme.PowerLoad => "Power-Load", + TapeProtectionScheme.EliteUniLoader => "Elite Uni-Loader", + TapeProtectionScheme.SoftLock => "SoftLock", + _ => scheme.ToString(), + }; + + /// + /// Detect a tape loading scheme from the parsed block list (best-effort, for reporting). + /// + public static TapeProtectionScheme Detect(IReadOnlyList blocks) + { + if (blocks == null || blocks.Count == 0) return TapeProtectionScheme.None; + + // Gather the signals in one pass. IMPORTANT (learned from validating against real dumps): many + // loaders are ENCRYPTED on tape, so their in-memory poll-loop opcodes are NOT present in the block + // data - only PLAINTEXT first-stage code (ROM-edge relocators) is matchable. The reliably-present + // signals are the PULSE ones (a turbo data block's bit-0/bit-1 cell timings, its flag byte, pilot + // width/count) plus block structure, since those are the physical encoding and cannot be encrypted. + bool novaload = false, call5e7 = false, edgeReloc = false, spReloc = false, bleep = false, + search = false, microprose = false, rollercoaster = false, rqfl = false, zydro = false, zeta = false; + foreach (var b in blocks) + { + var d = b.BlockData; + if (d == null || d.Length < 4) continue; + if (ContainsAscii(d, "PSS NOVALOAD")) novaload = true; + if (Contains(d, 0xCD, 0xE7, 0x05)) call5e7 = true; // CALL 05E7 (ROM edge routine) + if (Contains(d, 0x11, 0x00, 0xFF, 0x01, 0x00, 0x01, 0xED, 0xB0)) edgeReloc = true; // relocate loader to FF00 (The Edge) + if (Contains(d, 0x31, 0x01, 0xFF)) spReloc = true; // LD SP,#FF01 (Software Projects) + if (Contains(d, 0xDB, 0xFE, 0xA9, 0xE6, 0x40)) search = true; // IN A,(FE); XOR C; AND #40 (Search Loader, bit-6) + if (Contains(d, 0x32, 0x15, 0xFF)) bleep = true; // LD (#FF15),A self-mod (Bleepload, per MakeTZX source) + if (Contains(d, 0xDD, 0x21, 0x03, 0xF8, 0xFD, 0x21, 0x00, 0x60)) microprose = true; // LD IX,#F803; LD IY,#6000 (Microprose) + if (Contains(d, 0xD3, 0xFE, 0xDB, 0xFE, 0xE6, 0x20)) rollercoaster = true; // OUT (FE),A; IN A,(FE); AND #20 (Roller Coaster) + if (Contains(d, 0x21, 0x00, 0x58, 0x11, 0x01, 0x58, 0x01, 0xFF, 0x02, 0xED, 0xB0)) rqfl = true; // attr-clear LDIR (RQFL) + if (Contains(d, 0xFB, 0xED, 0x4D, 0xFB, 0xE1, 0xC9)) zydro = true; // IM2 handler EI;RETI;EI;POP HL;RET (Zydroload) + if (Contains(d, 0x10, 0xFF, 0x02, 0x00, 0x80)) zeta = true; // ZetaLoad table marker (MakeTZX source) + } + + // --- distinctive plaintext code signatures (most specific first) --- + if (novaload) return TapeProtectionScheme.Novaload; + if (zeta) return TapeProtectionScheme.ZetaLoad; + if (microprose) return TapeProtectionScheme.Microprose; + if (rqfl) return TapeProtectionScheme.Rqfl; + if (rollercoaster) return TapeProtectionScheme.RollerCoaster; + if (zydro) return TapeProtectionScheme.Zydroload; + if (call5e7 && edgeReloc) return TapeProtectionScheme.TheEdge; + if (call5e7 && spReloc) return TapeProtectionScheme.SoftwareProjects; + if (search) return TapeProtectionScheme.SearchLoader; + if (bleep) return TapeProtectionScheme.Bleepload; + + // Speedlock FIRST (before the Alkatraz bit test): Speedlock's ~555/1110 turbo bits are almost identical + // to Alkatraz's 560/1120, but only Speedlock carries the composite sync - key on that. The composite + // sync appears as either a ~2100T Pure_Tone run OR a Pulse_Sequence carrying the ~1420T mid-pulse + // (1420 = 2x710, neither a standard sync 667/735 nor any bit cell - unique to Speedlock's sync). + foreach (var b in blocks) + { + if (b.BlockDescription == BlockType.Pure_Tone && PilotPulseCount(b, 2100) >= 100) + return TapeProtectionScheme.Speedlock; + if (b.BlockDescription == BlockType.Pulse_Sequence && HasPulseNear(b, 1420)) + return TapeProtectionScheme.Speedlock; + } + + // Speedlock also breaks the load into MANY small blocks (real dumps: 140-565). Its ~560/1120 turbo + // bits are identical to Alkatraz's and Players', but those use only a handful of blocks (<=14 / ~9), + // so a high block count + 560/1120 bits reliably means Speedlock. (Its data is encrypted, so the flag + // byte varies and is not a tell.) Checked before the Alkatraz/Players bit tests below. + if (blocks.Count > 64) + foreach (var b in blocks) + if (IsDataBlock(b.BlockDescription)) + { + var (sb0, sb1) = BitCells(b); + if (Near(sb0, 560) && Near(sb1, 1120)) return TapeProtectionScheme.Speedlock; + } + + // --- FLAG-BYTE tells: scan EVERY block with real data (the loader's flag byte is encryption-proof + // and a non-00/FF flag can NOT occur on a normal ROM block, so it uniquely marks a custom loader). + // FTL in particular delivers its flag-99 data as a STANDARD-speed block on some rips, so we can't + // restrict this to turbo blocks. --- + bool f84 = false, f21 = false, f80 = false, f81 = false, f82 = false; + foreach (var b in blocks) + { + var d = b.BlockData; + if (d == null || d.Length <= 64) continue; + byte flag = d[0]; + var (fb0, fb1) = BitCells(b); + if (flag == 0x99 && fb1 > 1200) return TapeProtectionScheme.Ftl; // FTL/Gargoyle: flag #99 + if (flag == 0x98 && fb1 > 1200) return TapeProtectionScheme.PaulOwens; // Paul Owens: the #98 loader constant is the block flag + if (flag == 0xFD && b.BlockDescription == BlockType.Pure_Data_Block) return TapeProtectionScheme.DigitalIntegration; // DI: Pure Data block, flag #FD + if (flag == 0x07 && fb1 is >= 1300 and <= 1360) return TapeProtectionScheme.Novaload; // PSS Novaload: flag #07, bit ~670/1330 + // Power-Load and Elite Uni-Loader use fixed flag bytes on their data blocks - keyed on the + // COMBINATION (not any single byte) so encrypted-flag noise from other schemes can't false-match. + if (flag == 0x84) f84 = true; else if (flag == 0x21) f21 = true; + else if (flag == 0x80) f80 = true; else if (flag == 0x81) f81 = true; else if (flag == 0x82) f82 = true; + } + if (f84 && f21) return TapeProtectionScheme.PowerLoad; // Power-Load: flag #84 turbo + flag #21 data + if (f80 && f81 && f82) return TapeProtectionScheme.EliteUniLoader; // Elite Uni-Loader: sequential flags #80/#81/#82 + + // --- BIT-CELL tells on the turbo data blocks. Fast bit-timings collide across schemes, so the FLAG + // byte breaks the ties: Players' turbo flag is #00, Alkatraz's is always non-#00 (9B/65/24/A1/28...). + foreach (var b in blocks) + { + if (!IsDataBlock(b.BlockDescription)) continue; + var d = b.BlockData; + byte flag = d is { Length: > 0 } ? d[0] : (byte)0; + var (bit0, bit1) = BitCells(b); + if (call5e7 && Near(bit0, 465) && Near(bit1, 930)) return TapeProtectionScheme.Gremlin; // Gremlin 1: ROM-edge loader + 465/930 bits + if (b.BlockDescription == BlockType.Pure_Data_Block && Near(bit0, 680) && Near(bit1, 1340)) return TapeProtectionScheme.SoftLock; // SoftLock: Pure Data @680/1340 (Novaload uses Turbo+flag07, caught above) + if (flag == 0x00 && Near(bit0, 620) && Near(bit1, 1240)) return TapeProtectionScheme.Gremlin2; // Gremlin 2: 620/1240, flag #00 (vs Haxpoc 620/1220 flag #FF) + if (flag == 0x00 && bit0 is >= 555 and <= 605 && bit1 is >= 1115 and <= 1205) return TapeProtectionScheme.Players; // Players 1/2: 560-600/1120-1200, flag #00 + if (flag != 0x00 && ((Near(bit0, 560) && Near(bit1, 1120)) || (Near(bit0, 620) && Near(bit1, 1060)))) + return TapeProtectionScheme.Alkatraz; // Alkatraz: 560/1120 (or 720 Degrees' 620/1060), non-#00 flag + } + + // --- pulse pass: distinctive pilot widths and counts --- + foreach (var b in blocks) + { + int pilot = PilotPulseWidth(b); + if (pilot != 0 && Near(pilot, 1739)) return TapeProtectionScheme.Micromega; + } + // pilot-COUNT tells: a standard 2168T pilot but an unusual number of pulses + foreach (var b in blocks) + { + int cnt = PilotPulseCount(b, 2168); + if (cnt == 0) continue; + if (cnt is >= 8188 and <= 8199) return TapeProtectionScheme.Edos; // 8193 / 8194 pilot + if (cnt is >= 6885 and <= 6940) return TapeProtectionScheme.Moonlighter; // ~6912 pilot + } + + + // --- fallback: a tape made only of standard-speed blocks is a plain ROM load --- + bool anyData = false, allStandard = true; + foreach (var b in blocks) + { + switch (b.BlockDescription) + { + case BlockType.Standard_Speed_Data_Block: + anyData = true; + break; + case BlockType.Turbo_Speed_Data_Block: + case BlockType.Pure_Data_Block: + case BlockType.Generalized_Data_Block: + case BlockType.CSW_Recording: + case BlockType.WAV_Recording: + case BlockType.Direct_Recording: + anyData = true; + allStandard = false; + break; + } + } + if (anyData && allStandard) return TapeProtectionScheme.StandardRom; + + return TapeProtectionScheme.None; + } + + // The pilot tone is a long run of (near-)equal pulses. Return the pilot pulse width if a run of at least + // 256 near-equal pulses is present near the block start, else 0. Robust to the odd jittered value. + private static int PilotPulseWidth(TapeDataBlock b) + { + var p = b.DataPeriods; + if (p == null || p.Count < 256) return 0; + int runVal = 0, runLen = 0; + foreach (var v in p) + { + if (v > 0 && Near(v, runVal)) runLen++; + else { runVal = v; runLen = 1; } + if (runLen >= 256 && runVal > 100) return runVal; // a genuine pilot tone + } + return 0; + } + + // Length of the longest run of pulses near width (the pilot tone at the given width), + // so a scheme can be keyed on an unusual pilot COUNT at the standard 2168T width. 0 if none. + private static int PilotPulseCount(TapeDataBlock b, int width) + { + var p = b.DataPeriods; + if (p == null) return 0; + int best = 0, run = 0; + foreach (var v in p) + { + if (Near(v, width)) { run++; if (run > best) best = run; } + else run = 0; + } + return best; + } + + private static bool IsDataBlock(BlockType t) => t is BlockType.Turbo_Speed_Data_Block + or BlockType.Pure_Data_Block or BlockType.Generalized_Data_Block; + + // The two most common short pulse widths in a block = its bit-0 / bit-1 cell timings (returned low,high). + // A ZX bit is two equal pulses, so these values are directly comparable to the documented bit-0/bit-1. + private static (int Bit0, int Bit1) BitCells(TapeDataBlock b) + { + var p = b.DataPeriods; + if (p == null) return (0, 0); + var tally = new Dictionary(); + foreach (var v in p) + if (v is > 200 and < 2000) { int k = (v + 10) / 20 * 20; tally[k] = (tally.TryGetValue(k, out int c) ? c : 0) + 1; } + int a = 0, an = 0, bb = 0, bn = 0; + foreach (var kv in tally) + { + if (kv.Value > an) { bb = a; bn = an; a = kv.Key; an = kv.Value; } + else if (kv.Value > bn) { bb = kv.Key; bn = kv.Value; } + } + return a <= bb ? (a, bb) : (bb, a); + } + + // True if any pulse in the block is near target T-states. + private static bool HasPulseNear(TapeDataBlock b, int target) + { + var p = b.DataPeriods; + if (p == null) return false; + foreach (var v in p) if (Near(v, target)) return true; + return false; + } + + private static bool Near(int a, int b) => a > 0 && b > 0 && System.Math.Abs(a - b) <= 30; + + // Does the byte array contain the given contiguous opcode sequence anywhere? + private static bool Contains(byte[] data, params byte[] pattern) + { + if (data == null || data.Length < pattern.Length) return false; + for (int i = 0; i <= data.Length - pattern.Length; i++) + { + int j = 0; + for (; j < pattern.Length; j++) if (data[i + j] != pattern[j]) break; + if (j == pattern.Length) return true; + } + return false; + } + + private static bool ContainsAscii(byte[] data, string s) + { + if (data == null || data.Length < s.Length) return false; + for (int i = 0; i <= data.Length - s.Length; i++) + { + int j = 0; + for (; j < s.Length; j++) if (data[i + j] != (byte)s[j]) break; + if (j == s.Length) return true; + } + return false; + } + } +}