diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..426d76d --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..774a589 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ac_K + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d76e640 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# TTGamesExplorerRebirth + TTGames modding tool diff --git a/distribution/misc/Logo.png b/distribution/misc/Logo.png new file mode 100644 index 0000000..9040ad3 Binary files /dev/null and b/distribution/misc/Logo.png differ diff --git a/distribution/misc/Screen01.png b/distribution/misc/Screen01.png new file mode 100644 index 0000000..eea11ab Binary files /dev/null and b/distribution/misc/Screen01.png differ diff --git a/distribution/misc/Screen02.png b/distribution/misc/Screen02.png new file mode 100644 index 0000000..055fa8f Binary files /dev/null and b/distribution/misc/Screen02.png differ diff --git a/distribution/misc/Screen03.png b/distribution/misc/Screen03.png new file mode 100644 index 0000000..9e470a2 Binary files /dev/null and b/distribution/misc/Screen03.png differ diff --git a/distribution/misc/Screen04.png b/distribution/misc/Screen04.png new file mode 100644 index 0000000..36e58f1 Binary files /dev/null and b/distribution/misc/Screen04.png differ diff --git a/distribution/misc/Screen05.png b/distribution/misc/Screen05.png new file mode 100644 index 0000000..651bc5b Binary files /dev/null and b/distribution/misc/Screen05.png differ diff --git a/distribution/misc/Screen06.png b/distribution/misc/Screen06.png new file mode 100644 index 0000000..66fee08 Binary files /dev/null and b/distribution/misc/Screen06.png differ diff --git a/src/TTGamesExplorerRebirth.sln b/src/TTGamesExplorerRebirth.sln new file mode 100644 index 0000000..3520571 --- /dev/null +++ b/src/TTGamesExplorerRebirth.sln @@ -0,0 +1,163 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34511.84 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TTGamesExplorerRebirthCmd", "TTGamesExplorerRebirthCmd\TTGamesExplorerRebirthCmd.csproj", "{7371710E-E05A-4758-94A1-5CE01DD6BC29}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TTGamesExplorerRebirthLib", "TTGamesExplorerRebirthLib\TTGamesExplorerRebirthLib.csproj", "{6DE3EAD3-AF28-4585-9E6F-3B8B61773741}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TTGamesExplorerRebirthUI", "TTGamesExplorerRebirthUI\TTGamesExplorerRebirthUI.csproj", "{43ED4579-F874-4497-AA9D-B20CF3C9A59B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Main|Any CPU = Main|Any CPU + Main|ARM = Main|ARM + Main|ARM64 = Main|ARM64 + Main|x64 = Main|x64 + Main|x86 = Main|x86 + Main-Debug|Any CPU = Main-Debug|Any CPU + Main-Debug|ARM = Main-Debug|ARM + Main-Debug|ARM64 = Main-Debug|ARM64 + Main-Debug|x64 = Main-Debug|x64 + Main-Debug|x86 = Main-Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|ARM64.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x64.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x64.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x86.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Debug|x86.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|Any CPU.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|Any CPU.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM64.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|ARM64.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x64.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x64.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x86.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main|x86.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|Any CPU.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM64.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|ARM64.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x64.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x64.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x86.ActiveCfg = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Main-Debug|x86.Build.0 = Debug|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|Any CPU.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM64.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|ARM64.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x64.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x64.Build.0 = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x86.ActiveCfg = Release|Any CPU + {7371710E-E05A-4758-94A1-5CE01DD6BC29}.Release|x86.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|ARM64.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x64.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Debug|x86.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|Any CPU.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|Any CPU.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM64.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|ARM64.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x64.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x64.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x86.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main|x86.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|Any CPU.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM64.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|ARM64.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x64.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x64.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x86.ActiveCfg = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Main-Debug|x86.Build.0 = Debug|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|Any CPU.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM64.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|ARM64.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x64.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x64.Build.0 = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x86.ActiveCfg = Release|Any CPU + {6DE3EAD3-AF28-4585-9E6F-3B8B61773741}.Release|x86.Build.0 = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|ARM64.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x64.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x64.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x86.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Debug|x86.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|Any CPU.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|Any CPU.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM64.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|ARM64.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x64.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x64.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x86.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main|x86.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|Any CPU.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM64.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|ARM64.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x64.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x64.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x86.ActiveCfg = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Main-Debug|x86.Build.0 = Debug|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|Any CPU.Build.0 = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM.ActiveCfg = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM.Build.0 = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM64.ActiveCfg = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|ARM64.Build.0 = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x64.ActiveCfg = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x64.Build.0 = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x86.ActiveCfg = Release|Any CPU + {43ED4579-F874-4497-AA9D-B20CF3C9A59B}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3F7065D4-94E1-42E5-A498-3F8C4DE41FCE} + EndGlobalSection +EndGlobal diff --git a/src/TTGamesExplorerRebirthCmd/Program.cs b/src/TTGamesExplorerRebirthCmd/Program.cs new file mode 100644 index 0000000..f7e9319 --- /dev/null +++ b/src/TTGamesExplorerRebirthCmd/Program.cs @@ -0,0 +1,21 @@ +namespace TTGamesExplorerRebirthCmd +{ + internal class Program + { + static void Main() + { + Console.WriteLine("TTGames Explorer Rebirth"); + Console.WriteLine("------------------------\n"); + + /* + + This is used for testing format class easily before writing the UI part. + TODO: Maybe use this project do to a CLI project. + + */ + + Console.WriteLine("Done!"); + Console.ReadKey(); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthCmd/TTGamesExplorerRebirthCmd.csproj b/src/TTGamesExplorerRebirthCmd/TTGamesExplorerRebirthCmd.csproj new file mode 100644 index 0000000..f805b40 --- /dev/null +++ b/src/TTGamesExplorerRebirthCmd/TTGamesExplorerRebirthCmd.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + disable + + + + + + + diff --git a/src/TTGamesExplorerRebirthLib/Compression/UnLZ2K.cs b/src/TTGamesExplorerRebirthLib/Compression/UnLZ2K.cs new file mode 100644 index 0000000..33b9f13 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Compression/UnLZ2K.cs @@ -0,0 +1,558 @@ +namespace TTGamesExplorerRebirthLib.Compression +{ + public static class UnLZ2K + { + private const uint ChunkSizeConst = 0x2000; + + private static byte[] _srcBuffer; + private static byte[] _chunkBuffer; + private static byte[] _smallByteBuffer; + private static byte[] _largeByteBuffer; + private static short[] _smallWordBuffer; + private static short[] _largeWordBuffer; + private static short[] _parallelBuffer0; + private static short[] _parallelBuffer1; + + private static uint _bitStream; + private static byte _previousBitAlign; + private static byte _lastByteRead; + private static uint _srcOffset; + private static int _literalsToCopy; + private static short _chunksWithCurrentSetup; + private static uint _readOffset; + + /// + /// Give source byte buffer and destination byte buffer, process to LZ2K decompression. + /// This algorithm is mostly used by TTGames. + /// + /// + /// Based from RE of Lego Worlds by Ac_K. + /// Based on unlz2K by Trevor Natiuk: + /// https://github.com/pianistrevor/unlz2k + /// + /// + /// Byte array representing the source data to be decompressed. + /// + /// + /// Byte array representing the decompressed destination data. + /// + public static void Decompress(byte[] srcBuffer, byte[] dstBuffer) + { + if (dstBuffer.Length == 0) + { + return; + } + + _srcBuffer = srcBuffer; + _chunkBuffer = new byte[ChunkSizeConst]; + _smallByteBuffer = new byte[20]; + _largeByteBuffer = new byte[510]; + _smallWordBuffer = new short[256]; + _largeWordBuffer = new short[4096]; + _parallelBuffer0 = new short[1024]; + _parallelBuffer1 = new short[1024]; + + _bitStream = 0; + _previousBitAlign = 0; + _lastByteRead = 0; + _srcOffset = 0; + _literalsToCopy = 0; + _chunksWithCurrentSetup = 0; + _readOffset = 0; + + uint bytesLeft = (uint)dstBuffer.Length; + + using MemoryStream dstStream = new(dstBuffer); + using BinaryWriter dstWriter = new(dstStream); + + LoadIntoBitstream(0x20); + + while (bytesLeft != 0) + { + uint chunkSize = bytesLeft < ChunkSizeConst ? bytesLeft : ChunkSizeConst; + + DecodeChunk(chunkSize); + + byte[] tempBuffer = new byte[chunkSize]; + + Array.Copy(_chunkBuffer, tempBuffer, chunkSize); + + dstWriter.Write(tempBuffer); + + bytesLeft -= chunkSize; + } + + dstBuffer = dstStream.ToArray(); + } + + private static void LoadIntoBitstream(byte bits) + { + _bitStream <<= bits; + + if (bits > _previousBitAlign) + { + do + { + bits -= _previousBitAlign; + _bitStream |= (uint)_lastByteRead << bits; + _lastByteRead = (byte)((_srcOffset == (uint)_srcBuffer.Length) ? 0 : _srcBuffer[_srcOffset++]); + _previousBitAlign = 8; + } while (bits > _previousBitAlign); + } + + _previousBitAlign -= bits; + _bitStream |= (uint)_lastByteRead >> _previousBitAlign; + _bitStream &= 0xFFFFFFFF; + } + + private static void DecodeChunk(uint chunkSize) + { + uint dstOffset = 0; + + --_literalsToCopy; + if (_literalsToCopy >= 0) + { + while (_literalsToCopy >= 0) + { + _chunkBuffer[dstOffset++] = _chunkBuffer[_readOffset++]; + _readOffset &= 0x1FFF; + + if (dstOffset == chunkSize) + { + return; + } + + --_literalsToCopy; + } + } + + while (dstOffset < chunkSize) + { + short decodedBitStream = DecodeBitStream(); + if (decodedBitStream <= 255) + { + _chunkBuffer[dstOffset++] = (byte)decodedBitStream; + + if (dstOffset == chunkSize) + { + return; + } + } + else + { + _literalsToCopy = DecodeBitStreamForLiterals(); + _readOffset = (uint)(dstOffset - _literalsToCopy - 1) & 0x1FFF; + _literalsToCopy = decodedBitStream - 254; + + while (_literalsToCopy >= 0) + { + _chunkBuffer[dstOffset++] = _chunkBuffer[_readOffset++]; + _readOffset &= 0x1FFF; + + if (dstOffset == chunkSize) + { + return; + } + + --_literalsToCopy; + } + } + } + + if (dstOffset > chunkSize) + { + throw new IndexOutOfRangeException(); + } + } + + private static short DecodeBitStream() + { + if (_chunksWithCurrentSetup == 0) + { + _chunksWithCurrentSetup = (short)(_bitStream >> 16); + + LoadIntoBitstream(0x10); + FillSmallDicts(19, 5, 3); + FillLargeDicts(); + FillSmallDicts(14, 4, -1); + } + + _chunksWithCurrentSetup--; + + short tmpValue = _largeWordBuffer[_bitStream >> 20]; + if (tmpValue >= _largeByteBuffer.Length) + { + uint mask = 0x80000; + + while (tmpValue >= _largeByteBuffer.Length) + { + tmpValue = ((_bitStream & mask) == 0) ? _parallelBuffer0[tmpValue] : _parallelBuffer1[tmpValue]; + mask >>= 1; + } + } + + LoadIntoBitstream(_largeByteBuffer[tmpValue]); + + return tmpValue; + } + + private static int DecodeBitStreamForLiterals() + { + short tmpValue = _smallWordBuffer[_bitStream >> 24]; + if (tmpValue >= 14) + { + uint mask = 0x800000; + + while (tmpValue >= 14) + { + tmpValue = ((_bitStream & mask) == 0) ? _parallelBuffer0[tmpValue] : _parallelBuffer1[tmpValue]; + mask >>= 1; + } + } + + LoadIntoBitstream(_smallByteBuffer[tmpValue]); + + if (tmpValue == 0) + { + return 0; + } + + if (tmpValue == 1) + { + return 2; + } + + tmpValue--; + + uint tmpBitStream = _bitStream >> (32 - tmpValue); + + LoadIntoBitstream((byte)tmpValue); + + return (int)(tmpBitStream + (1 << tmpValue)); + } + + private static void FillSmallDicts(byte length, byte bits, sbyte specialInd) + { + byte tmpValue1 = (byte)(_bitStream >> (32 - bits)); // NOTE: bits is never 0. + + LoadIntoBitstream(bits); + + if (tmpValue1 != 0) + { + byte tmpValue2 = 0; + + if (tmpValue1 > 0) + { + while (tmpValue2 < tmpValue1) + { + uint tmpBitStream = (byte)(_bitStream >> 29); + bits = 3; + + if (tmpBitStream == 7) + { + uint mask = 0x10000000; + + if ((_bitStream & mask) == 0) + { + bits = 4; + } + else + { + byte counter = 0; + + while ((_bitStream & mask) != 0) + { + mask >>= 1; + counter += 1; + } + + bits = (byte)(counter + 4); + tmpBitStream += counter; + } + } + + LoadIntoBitstream(bits); + + _smallByteBuffer[tmpValue2] = (byte)tmpBitStream; + + tmpValue2++; + + if (tmpValue2 == specialInd) + { + sbyte specialLength = (sbyte)(_bitStream >> 30); + + LoadIntoBitstream(2); + + if (specialLength >= 1) + { + Array.Clear(_smallByteBuffer, tmpValue2, specialLength); + + tmpValue2 += (byte)specialLength; + } + } + } + } + + if (tmpValue2 < length) + { + Array.Clear(_smallByteBuffer, tmpValue2, length - tmpValue2); + } + + FillWordsUsingBytes(length, _smallByteBuffer, 8, _smallWordBuffer); + + return; + } + + tmpValue1 = (byte)(_bitStream >> (32 - bits)); + + LoadIntoBitstream(bits); + + if (length > 0) + { + Array.Clear(_smallByteBuffer, 0, length); + } + + for (int i = 0; i < _smallWordBuffer.Length; i++) + { + _smallWordBuffer[i] = tmpValue1; + } + } + + private static void FillLargeDicts() + { + short tmpValue1 = (short)(_bitStream >> 23); + + LoadIntoBitstream(9); + + if (tmpValue1 == 0) + { + short tmpValue2 = (short)(_bitStream >> 23); + + LoadIntoBitstream(9); + + Array.Clear(_largeByteBuffer, 0, _largeByteBuffer.Length); + + for (int i = 0; i < _largeWordBuffer.Length; i++) + { + _largeWordBuffer[i] = tmpValue2; + } + + return; + } + + ushort bytes = 0; + if (tmpValue1 < 0) + { + // NOTE: Does this ever happen? + + Array.Clear(_largeByteBuffer, 0, _largeByteBuffer.Length); + + FillWordsUsingBytes((short)_largeByteBuffer.Length, _largeByteBuffer, 12, _largeWordBuffer); + + return; + } + + while (bytes < tmpValue1) + { + ushort tmpLength = (ushort)(_bitStream >> 24); + short tmpValue2 = _smallWordBuffer[tmpLength]; + + if (tmpValue2 >= 19) + { + uint mask = 0x800000; + + do + { + tmpValue2 = ((_bitStream & mask) == 0) ? _parallelBuffer0[tmpValue2] : _parallelBuffer1[tmpValue2]; + mask >>= 1; + } while (tmpValue2 >= 19); + } + + LoadIntoBitstream(_smallByteBuffer[tmpValue2]); + + if (tmpValue2 > 2) + { + tmpValue2 -= 2; + _largeByteBuffer[bytes++] = (byte)tmpValue2; + } + else + { + if (tmpValue2 == 0) + { + tmpLength = 1; + } + else if (tmpValue2 == 1) + { + tmpValue2 = (short)(_bitStream >> 28); + + LoadIntoBitstream(4); + + tmpLength = (ushort)(tmpValue2 + 3); + } + else + { + tmpValue2 = (short)(_bitStream >> 23); + + LoadIntoBitstream(9); + + tmpLength = (ushort)(tmpValue2 + 20); + } + + if (tmpLength > 0) + { + Array.Clear(_largeByteBuffer, bytes, tmpLength); + + bytes += tmpLength; + } + } + } + + if (bytes < _largeByteBuffer.Length) + { + Array.Clear(_largeByteBuffer, bytes, _largeByteBuffer.Length - bytes); + } + + FillWordsUsingBytes((short)_largeByteBuffer.Length, _largeByteBuffer, 12, _largeWordBuffer); + + return; + } + + private static void FillWordsUsingBytes(short bytesLength, byte[] bytesBuffer, short pivot, short[] wordsBuffer) + { + ushort[] srcBuffer = new ushort[17]; + ushort[] destBuffer = new ushort[18]; + + for (int i = 0; i < bytesLength; i++) + { + srcBuffer[bytesBuffer[i]]++; + } + + sbyte shift = 14; + sbyte ind = 1; + ushort low, high; + + while (ind <= 16) + { + low = srcBuffer[ind]; + high = srcBuffer[ind + 1]; + low <<= shift + 1; + high <<= shift; + low += destBuffer[ind]; + ind += 4; + high += low; + high &= 0xFFFF; + + destBuffer[ind - 3] = low; + destBuffer[ind - 2] = high; + + low = (ushort)(srcBuffer[ind - 2] << (shift - 1)); + low += high; + high = (ushort)(srcBuffer[ind - 1] << (shift - 2)); + high += low; + + destBuffer[ind - 1] = low; + destBuffer[ind] = high; + + shift -= 4; + } + + if (destBuffer[17] != 0) + { + throw new Exception("Wrong table."); + } + + shift = (sbyte)(pivot - 1); + + int tmpValue = 16 - pivot; + int tmpValueCopy = tmpValue; + + for (int i = 1; i <= pivot; ++i) + { + destBuffer[i] >>= tmpValue; + srcBuffer[i] = (ushort)(1 << shift--); + } + + tmpValue--; + + for (int i = pivot + 1; i <= 16; ++i) + { + srcBuffer[i] = (ushort)(1 << tmpValue--); + } + + ushort comp1 = destBuffer[pivot + 1]; + comp1 >>= 16 - pivot; + + if (comp1 != 0) + { + ushort comp2 = (ushort)(1 << pivot); + + if (comp1 != comp2) + { + for (int i = comp1; i < comp2; i++) + { + wordsBuffer[i] = 0; + } + } + } + + if (bytesLength <= 0) + { + return; + } + + shift = (sbyte)(15 - pivot); + + ushort mask = (ushort)(1 << shift); + short tmpValue2 = bytesLength; + + for (int i = 0; i < bytesLength; i++) + { + byte tmpByte = bytesBuffer[i]; + if (tmpByte != 0) + { + ushort destValue = destBuffer[tmpByte]; + ushort srcValue = srcBuffer[tmpByte]; + + srcValue += destValue; + + if (tmpByte > pivot) + { + short[] tmpBuffer = wordsBuffer; + short tmpOffset = (short)(destValue >> tmpValueCopy); + byte newLength = (byte)(tmpByte - pivot); + + if (newLength != 0) + { + while (newLength != 0) + { + if (tmpBuffer[tmpOffset] == 0) + { + _parallelBuffer0[tmpValue2] = 0; + _parallelBuffer1[tmpValue2] = 0; + + tmpBuffer[tmpOffset] = tmpValue2++; + } + + tmpOffset = tmpBuffer[tmpOffset]; + tmpBuffer = ((destValue & mask) == 0) ? _parallelBuffer0 : _parallelBuffer1; + destValue += destValue; + newLength--; + } + } + + tmpBuffer[tmpOffset] = (short)i; + } + else if (destValue < srcValue) + { + for (int j = destValue; j < srcValue; j++) + { + wordsBuffer[j] = (short)i; + } + } + + destBuffer[tmpByte] = srcValue; + } + } + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Encryption/RC4.cs b/src/TTGamesExplorerRebirthLib/Encryption/RC4.cs new file mode 100644 index 0000000..73ef079 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Encryption/RC4.cs @@ -0,0 +1,95 @@ +namespace TTGamesExplorerRebirthLib.Encryption +{ + public static class RC4 + { + /// + /// Give data and an encryption key, apply RC4 cryptography. RC4 is symmetric, + /// which means this single method will work for encrypting and decrypting. + /// + /// + /// Original implementation by Christopher Whitley: + /// https://github.com/manbeardgames/RC4 + /// + /// https://en.wikipedia.org/wiki/RC4 + /// + /// + /// Byte array representing the data to be encrypted/decrypted. + /// + /// + /// Byte array representing the key to use. + /// + /// + /// Byte array representing the encrypted/decrypted data. + /// + public static byte[] Crypt(byte[] data, byte[] key) + { + // Key Scheduling Algorithm Phase: + // KSA Phase Step 1: First, the entries of S are set equal to the values of 0 to 255 in ascending order. + int[] s = new int[256]; + for (int _ = 0; _ < 256; _++) + { + s[_] = _; + } + + // KSA Phase Step 2a: Next, a temporary vector T is created. + int[] t = new int[256]; + + // KSA Phase Step 2b: If the length of the key k is 256 bytes, then k is assigned to T. + if (key.Length == 256) + { + Buffer.BlockCopy(key, 0, t, 0, key.Length); + } + else + { + // Otherwise, for a key with a given length, copy the elements of + // the key into vector T, repeating for as many times as neccessary to fill T. + for (int _ = 0; _ < 256; _++) + { + t[_] = key[_ % key.Length]; + } + } + + // KSA Phase Step 3: We use T to produce the initial permutation of S... + int i, j = 0; + for (i = 0; i < 256; i++) + { + // Increment j by the sum of S[i] and T[i], however keeping it within the + // range of 0 to 255 using mod (%) division. + j = (j + s[i] + t[i]) % 256; + + // Swap the values of S[i] and S[j] + (s[j], s[i]) = (s[i], s[j]); + } + + // Pseudo random generation algorithm (Stream Generation): + // Once the vector S is initialized from above in the Key Scheduling Algorithm Phase, + // the input key is no longer used. In this phase, for the length of the data, we... + i = j = 0; + byte[] result = new byte[data.Length]; + for (int iteration = 0; iteration < data.Length; iteration++) + { + // PRGA Phase Step 1. Continously increment i from 0 to 255, starting it back + // at 0 once we go beyond 255 (this is done with mod (%) division. + i = (i + 1) % 256; + + // PRGA Phase Step 2. Lookup the i'th element of S and add it to j, keeping the + // result within the range of 0 to 255 using mod (%) division. + j = (j + s[i]) % 256; + + // PRGA Phase Step 3. Swap the values of S[i] and S[j]. + (s[j], s[i]) = (s[i], s[j]); + + // PRGA Phase Step 4. Use the result of the sum of S[i] and S[j], mod (%) by 256, + // to get the index of S that handls the value of the stream value K. + int k = s[(s[i] + s[j]) % 256]; + + // PRGA Phase Step 5. Use bitwise exclusive OR (^) with the next byte in the data to + // produce the next byte of the resulting ciphertext (when + // encrypting) or plaintext (when decrypting). + result[iteration] = Convert.ToByte(data[iteration] ^ k); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/CBX.cs b/src/TTGamesExplorerRebirthLib/Formats/CBX.cs new file mode 100644 index 0000000..c5acd79 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/CBX.cs @@ -0,0 +1,486 @@ +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + /// + /// Give cbx file data, convert it to wav format. + /// + /// + /// Based on CBXDecoder (unlicensed) by Connor Harrison: + /// https://github.com/connorh315/CBXDecoder + /// + /// Code optimized by Ac_K. + /// + public class CBX + { + private const string MagicBox = "!B0X"; + private const int ChunkSize = 0x1B0; + + private readonly float[] _exponentTable = new float[64]; + + private readonly float[] _floatTable = [ + 0.000000f, -0.996776f, -0.990327f, -0.983879f, -0.977431f, -0.970982f, -0.964534f, -0.958085f, + -0.951637f, -0.930754f, -0.904960f, -0.879167f, -0.853373f, -0.827579f, -0.801786f, -0.775992f, + -0.750198f, -0.724405f, -0.698611f, -0.670635f, -0.619048f, -0.567460f, -0.515873f, -0.464286f, + -0.412698f, -0.361111f, -0.309524f, -0.257937f, -0.206349f, -0.154762f, -0.103175f, -0.051587f, + 0.000000f, 0.051587f, 0.103175f, 0.154762f, 0.206349f, 0.257937f, 0.309524f, 0.361111f, + 0.412698f, 0.464286f, 0.515873f, 0.567460f, 0.619048f, 0.670635f, 0.698611f, 0.724405f, + 0.750198f, 0.775992f, 0.801786f, 0.827579f, 0.853373f, 0.879167f, 0.904960f, 0.930754f, + 0.951637f, 0.958085f, 0.964534f, 0.970982f, 0.977431f, 0.983879f, 0.990327f, 0.996776f, + ]; + + private readonly byte[] _smallTable = [ + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x15, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x19, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x16, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x00, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x15, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x1A, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x11, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x16, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0D, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x12, + 0x04, 0x06, 0x05, 0x09, 0x04, 0x06, 0x05, 0x0E, 0x04, 0x06, 0x05, 0x0A, 0x04, 0x06, 0x05, 0x02, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1B, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x01, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1C, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x03, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1B, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x01, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x17, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x1C, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x13, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x18, + 0x04, 0x0B, 0x07, 0x0F, 0x04, 0x0C, 0x08, 0x14, 0x04, 0x0B, 0x07, 0x10, 0x04, 0x0C, 0x08, 0x03, + ]; + + private readonly int[] _wordTable = [ + 1, 8, 0, 1, 7, 0, 0, 8, 0, 0, 7, 0, 0, 2, 0, 0, + 2, -1082130432, 0, 2, 1065353216, 0, 3, -1082130432, 0, 3, 1065353216, 1, 4, -1073741824, 1, 4, + 1073741824, 1, 3, -1073741824, 1, 3, 1073741824, 1, 5, -1069547520, 1, 5, 1077936128, 1, 4, -1069547520, + 1, 4, 1077936128, 1, 6, -1065353216, 1, 6, 1082130432, 1, 5, -1065353216, 1, 5, 1082130432, 1, + 7, -1063256064, 1, 7, 1084227584, 1, 6, -1063256064, 1, 6, 1084227584, 1, 8, -1061158912, 1, 8, + 1086324736, 1, 7, -1061158912, 1, 7, 1086324736, 885592027, 1243806083, 1050253722, 20, 1065353216, 1065353216, 1036831949, 1061997773, 1117782016, + 1117782016, 1031127695, 1050253722, 1045220557, 1058642330, 1061997773, 1065353216, 23896536, 0, 4194304, 0, 149718852, 149718852, 0, 27225808, 27225816, + 1, 0, -1082130432, 1077936128, -1069547520, 1065353216, 1077936128, -1061158912, 1077936128, 0, -1069547520, 1077936128, 0, 0, 1065353216, 0, + 0, 0, 24903876, 0, + ]; + + private int _offset = 0; + private int _activeByte = 0; + private int _comparison = 8; + + private byte[] _chunkBuffer; + + private readonly float[] _firstLookup = new float[12]; + private readonly float[] _lookup = new float[12]; + private readonly float[] _decodeTable = new float[1000]; + private readonly float[] _collection = new float[120]; + private readonly float[] _data = new float[348]; + private readonly float[] _espResult = new float[12]; + private readonly float[] _otherResult = new float[12]; + + public byte[] WaveBuffer { get; private set; } + + public CBX(string filePath) + { + Convert(File.ReadAllBytes(filePath)); + } + + public CBX(byte[] buffer) + { + Convert(buffer); + } + + private void Convert(byte[] buffer) + { + // Initialize. + + for (int i = 0; i < _exponentTable.Length; ++i) + { + _exponentTable[i] = (float)(59.9246f * Math.Pow(1.068f, i + 1)); + } + + // Read Header. + + using MemoryStream stream = new(buffer); + using BinaryReader reader = new(stream); + + if (reader.ReadUInt32AsString() != MagicBox) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + int outputSize = reader.ReadInt32(); + int sampleRate = reader.ReadInt32(); + + // Convert the file to WAV. + + _chunkBuffer = reader.ReadBytes((int)(stream.Length - stream.Position)); + _activeByte = _offset + 1 > _chunkBuffer.Length ? (byte)0 : _chunkBuffer[_offset++]; + + int chunkOffset = 0; + ushort[] chunkData = new ushort[outputSize]; + + while (_offset < _chunkBuffer.Length) + { + GenerateDecodeTable(); + + for (int i = 0; i < ChunkSize; i++) + { + uint value = BitConverter.SingleToUInt32Bits(_decodeTable[i]) & 0x1FFFF; + if (value - 0x8000 < 0x10000) + { + value = (uint)(0x8000 - (value < 0x10000 ? 1 : 0)); + } + + chunkData[chunkOffset + i] = (ushort)value; + + _decodeTable[i] = 0.0f; + } + + chunkOffset += ChunkSize; + } + + // Write WAV header. + + using MemoryStream outputStream = new(); + using BinaryWriter writer = new(outputStream); + + writer.WriteStringWithoutPrefixedSize("RIFF"); + writer.Write(2 * chunkOffset + 32); + writer.WriteStringWithoutPrefixedSize("WAVE"); + writer.WriteStringWithoutPrefixedSize("fmt "); + writer.Write(16); + writer.Write((short)1); + writer.Write((short)1); + writer.Write(sampleRate); + writer.Write(sampleRate * 2); + writer.Write((short)2); + writer.Write((short)16); + writer.WriteStringWithoutPrefixedSize("data"); + writer.Write(2 * chunkOffset); + + // Write WAV data. + + for (int i = 0; i < chunkOffset; i++) + { + writer.Write(chunkData[i]); + } + + WaveBuffer = outputStream.ToArray(); + } + + private void RefreshActiveByte(int value) + { + _comparison -= value; + _activeByte >>= value; + + if (_comparison >= 8) + { + return; + } + + _activeByte |= (_offset + 1 > _chunkBuffer.Length ? (byte)0 : _chunkBuffer[_offset++]) << _comparison; + _comparison += 8; + } + + private void GenerateDecodeTable() + { + bool unknownFlag = (_activeByte & 63) < 24; + + for (int i = 0; i < 4; i++) + { + _lookup[i] = (float)((_floatTable[_activeByte & 63] - _data[i]) * 0.25); + + RefreshActiveByte(6); + } + + for (int i = 4; i < 12; i++) + { + _lookup[i] = (float)((_floatTable[(_activeByte & 31) + 16] - _data[i]) * 0.25); + + RefreshActiveByte(5); + } + + int unk0 = 0; + + for (int i = 216; i < 648; i += 108) + { + int unk1 = _activeByte & byte.MaxValue; + + RefreshActiveByte(8); + + int offset = i - unk1; + float unk2 = (_activeByte & 15) * 0.06666667f; + + RefreshActiveByte(4); + + float unk3 = _exponentTable[_activeByte & 63]; + + RefreshActiveByte(6); + + int unk4 = _activeByte & 1; + + RefreshActiveByte(1); + + int unk5 = _activeByte & 1; + + RefreshActiveByte(1); + + int collectionOffsetBase = 8; + + if (!unknownFlag) + { + int unk6 = 0; + + do + { + switch (_activeByte & 3) + { + case 0: + case 2: + { + _collection[collectionOffsetBase - 1 + unk4 + unk6] = 0.0f; + + RefreshActiveByte(1); + break; + } + case 1: + { + _collection[collectionOffsetBase - 1 + unk4 + unk6] = -2f; + + RefreshActiveByte(2); + break; + } + case 3: + { + _collection[collectionOffsetBase - 1 + unk4 + unk6] = 2f; + + RefreshActiveByte(2); + break; + } + } + + unk6 += 2; + } + while (unk6 < 108); + } + else + { + int unk7 = 0; + int unk8 = 0; + + do + { + byte unk9 = _smallTable[(_activeByte & byte.MaxValue) + (unk7 << 8)]; + unk7 = _wordTable[unk9 * 3]; + + RefreshActiveByte(_wordTable[(unk9 * 3) + 1]); + + if (unk9 < 4) + { + if (unk9 < 2) + { + int unk10 = 7; + + while (true) + { + int unk11 = _activeByte & 1; + + RefreshActiveByte(1); + + if (unk11 == 1) + { + unk10++; + } + else + { + break; + } + } + + int unk12 = _activeByte & 1; + + RefreshActiveByte(1); + + _collection[collectionOffsetBase - 1 + unk4 + unk8] = unk12 != 1 ? -unk10 : unk10; + + unk8 += 2; + } + else + { + int unk13 = (_activeByte & 63) + 7; + + RefreshActiveByte(6); + + if (unk13 * 2 + unk8 > 108) + { + unk13 = (108 - unk8) / 2; + } + + if (unk13 > 0) + { + do + { + _collection[collectionOffsetBase - 1 + unk4 + unk8] = 0.0f; + unk8 += 2; + unk13--; + } + while (unk13 != 0); + } + } + } + else + { + _collection[collectionOffsetBase - 1 + unk4 + unk8] = BitConverter.Int32BitsToSingle(_wordTable[(unk9 * 3) + 2]); + unk8 += 2; + } + } + while (unk8 <= 107); + } + + if (unk5 == 0) + { + for (int j = collectionOffsetBase + 1 - unk4; j < 107 + collectionOffsetBase + 1 - unk4; j += 2) + { + _collection[j - 1] = (_collection[j - 2] + _collection[j]) * 0.597385942935944f - (_collection[j - 4] + _collection[j + 2]) * 0.114591561257839f + (_collection[j - 6] + _collection[j + 4]) * 0.0180326793342829f; + } + + unk3 *= 0.5f; + } + else + { + for (int j = 0; j < 54; ++j) + { + _collection[collectionOffsetBase - unk4 + j * 2] = 0.0f; + } + } + + int unk14 = offset + 1; + int collectionOffset = collectionOffsetBase; + + for (int j = 0; j < 12; ++j) + { + for (int k = 0; k < 9; k++) + { + _decodeTable[unk0 + k] = _collection[collectionOffset + (k - 1)] * unk3 + (unk14 + (k - 1) >= 324 ? _decodeTable[unk14 + 27 - (352 - k)] : _data[unk14 + 27 + (k - 4)]) * unk2; + } + + collectionOffset += 9; + unk14 += 9; + unk0 += 9; + } + } + + for (int i = 0; i < 324; i++) + { + _data[24 + i] = _decodeTable[108 + i]; + } + + int unk15 = 0; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j != 12; j += 6) + { + for (int k = 0; k < 6; k++) + { + _data[j + k] += _lookup[j + k]; + } + } + + int unk16 = i != 3 ? 1 : 33; + + for (int j = 11; j > 0; j--) + { + _firstLookup[j] = _data[j - 1]; + } + + _firstLookup[0] = 1.0f; + + for (int j = 0; j < 12; j++) + { + float unk17 = -_data[11] * _firstLookup[11] - _data[10] * _firstLookup[10]; + + for (int k = 10; k > 0; k--) + { + _firstLookup[k + 1] = _firstLookup[k] + _data[k] * unk17; + unk17 -= _data[k - 1] * _firstLookup[k - 1]; + } + + _firstLookup[1] = _firstLookup[0] + _data[0] * unk17; + _firstLookup[0] = unk17; + _espResult[j] = unk17; + + int unk18 = 0; + if (j > 3) + { + int unk19 = (j - 4 >> 2) + 1; + unk18 = unk19 * 4; + int unk20 = 0; + do + { + unk19--; + unk17 = unk17 - _espResult[j - 1 - unk20 * 4] * _otherResult[unk20 * 4] - _espResult[j - 2 - unk20 * 4] * _otherResult[unk20 * 4 + 1] - _espResult[j - 3 - unk20 * 4] * _otherResult[unk20 * 4 + 2] - _espResult[j - 4 - unk20 * 4] * _otherResult[unk20 * 4 + 3]; + unk20++; + } + while (unk19 != 0); + } + + if (j > unk18) + { + do + { + unk17 -= _espResult[j - unk18 - 1] * _otherResult[unk18]; + ++unk18; + } + while (j > unk18); + } + + _otherResult[j] = unk17; + } + + if (unk16 <= 0) + { + return; + } + + do + { + List listIndex = [23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12]; + + int checkIndex(int i) + { + if (i == listIndex.Count) + { + i = 0; + } + + return i % listIndex.Count; + } + + for (int k = 0; k < 12; k++) + { + + _data[listIndex[checkIndex(k + 12)]] = (_data[listIndex[checkIndex(k + 11)]] * _otherResult[0] + _decodeTable[unk15 + k] + _data[listIndex[checkIndex(k + 10)]] * _otherResult[1] + _data[listIndex[checkIndex(k + 9)]] * _otherResult[2] + _data[listIndex[checkIndex(k + 8)]] * _otherResult[3] + _data[listIndex[checkIndex(k + 7)]] * _otherResult[4] + _data[listIndex[checkIndex(k + 6)]] * _otherResult[5] + _data[listIndex[checkIndex(k + 5)]] * _otherResult[6] + _data[listIndex[checkIndex(k + 4)]] * _otherResult[7] + _data[listIndex[checkIndex(k + 3)]] * _otherResult[8] + _data[listIndex[checkIndex(k + 2)]] * _otherResult[9] + _data[listIndex[checkIndex(k + 1)]] * _otherResult[10] + _data[listIndex[checkIndex(k)]] * _otherResult[11]); + _decodeTable[unk15 + k] = _data[listIndex[checkIndex(k + 12)]] + 12582912f; + } + + unk15 += 12; + --unk16; + } + while (unk16 != 0); + } + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/DAT/CompressionFormat.cs b/src/TTGamesExplorerRebirthLib/Formats/DAT/CompressionFormat.cs new file mode 100644 index 0000000..224a5fb --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DAT/CompressionFormat.cs @@ -0,0 +1,10 @@ +namespace TTGamesExplorerRebirthLib.Formats.DAT +{ + public enum CompressionFormat + { + None, + LZ2K, + ZIPX, + Deflate_v1, + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/DAT/DATArchive.cs b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATArchive.cs new file mode 100644 index 0000000..20cd2c1 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATArchive.cs @@ -0,0 +1,454 @@ +using TTGamesExplorerRebirthLib.Hash; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats.DAT +{ + /// + /// Give DAT file path and get a list of contained files through "Files" field. + /// + /// Files can be extracted using "ExtractFile" method. + /// + /// + /// Based on QuickBMS script by Luigi Auriemma: + /// https://aluigi.altervista.org/quickbms.htm + /// + /// Based on my own research (Ac_K). + /// + /// Games supported: + /// - LEGO The Lord of the Rings + /// - LEGO Star Wars - The Complete Saga + /// - LEGO Worlds + /// + /// + public class DATArchive + { + public const string MagicCC40TAD = ".CC40TAD"; + + public List Files = []; + + public string ArchiveFilePath; + + private struct TempFile + { + public ulong Offset; + public uint Size; + public uint CompressedSize; + public CompressionFormat Compression; + } + + // TODO: Improve the deserializer. + public DATArchive(string archiveFilePath) + { + ArchiveFilePath = archiveFilePath; + + using FileStream stream = new(archiveFilePath, FileMode.Open, FileAccess.Read); + using BinaryReader reader = new(stream); + + // Read DAT header. + + uint infoTableOffset = reader.ReadUInt32(); + + if ((infoTableOffset & 0x80000000) != 0) + { + infoTableOffset ^= 0xFFFFFFFF; + infoTableOffset <<= 8; + infoTableOffset += 0x100; + } + + uint infoTableSize = reader.ReadUInt32(); + + stream.Seek(infoTableOffset, SeekOrigin.Begin); + + uint versionType1 = reader.ReadUInt32(); + uint versionType2 = reader.ReadUInt32(); + + stream.Seek(infoTableOffset, SeekOrigin.Begin); + + // TODO: Improve this check. + if (versionType1.ToConvertedString() == "4CC." || + versionType1.ToConvertedString() == ".CC4" || + versionType2.ToConvertedString() == "4CC." || + versionType2.ToConvertedString() == ".CC4") + { + // Read DAT info table. + + uint datInfoTableSize = reader.ReadUInt32(); + + if (reader.ReadUInt64AsString() != MagicCC40TAD) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + int formatByteOrder1 = reader.ReadInt32BigEndian(); // -11 + uint formatVersion = reader.ReadUInt32BigEndian(); + uint filesCount1 = reader.ReadUInt32BigEndian(); + uint namesCount = reader.ReadUInt32BigEndian(); + uint namesSize = reader.ReadUInt32BigEndian(); + ulong namesOffset = (ulong)stream.Position; + + stream.Seek(namesSize, SeekOrigin.Current); + + ushort unknown2 = reader.ReadUInt16BigEndian(); // 0x1000 + ushort unknown3 = reader.ReadUInt16BigEndian(); // -1 + + // Read file names table. + + uint lastNameWorkaround = namesCount - 1; + ushort myId = 0; + + string[] namesTemp = new string[namesCount]; + string[] namesList = new string[filesCount1]; + + for (int i = 0; i < namesCount; i++) + { + uint nameOffset = reader.ReadUInt32BigEndian(); + ushort folderId = reader.ReadUInt16BigEndian(); + ushort unknown1Id = reader.ReadUInt16BigEndian(); + ushort unknown2Id = reader.ReadUInt16BigEndian(); + ushort fileId = reader.ReadUInt16BigEndian(); + + if (nameOffset != 0xFFFFFFFF) + { + long latestOffset = stream.Position; + + stream.Seek((long)namesOffset + nameOffset, SeekOrigin.Begin); + + string fileName = stream.ReadNullTerminatedString(); + + stream.Seek(latestOffset, SeekOrigin.Begin); + + // TODO: Find a proper solution. + if (i == lastNameWorkaround) + { + fileId = myId; + } + + fileName = $"{namesTemp[folderId]}\\{fileName}"; + + if (fileId != 0) + { + namesList[myId] = fileName; + myId++; + } + else + { + namesTemp[i] = fileName; + } + } + } + + // Read files info table. + + int formatByteOrder2 = reader.ReadInt32BigEndian(); // -11 / Version ? + uint filesCount2 = reader.ReadUInt32BigEndian(); + + if (filesCount2 != filesCount1) + { + throw new IndexOutOfRangeException($"{stream.Position:x8}"); + } + + TempFile[] tempFilesList = new TempFile[filesCount2]; + + for (int i = 0; i < filesCount2; i++) + { + tempFilesList[i].Offset = (formatByteOrder2 <= -11) ? reader.ReadUInt64BigEndian() : reader.ReadUInt32BigEndian(); + + uint compressedSize = reader.ReadUInt32BigEndian(); + uint size = reader.ReadUInt32BigEndian(); + + // TODO: Rework this once more format versions will be added. + uint compressed; + if (formatByteOrder2 <= -11) + { + compressed = size; + size &= 0x7FFFFFFF; + compressed >>= 31; + } + else + { + compressed = reader.ReadByte(); + + ushort zero = reader.ReadUInt16(); + + tempFilesList[i].Offset |= (uint)(reader.ReadByte() << 8); + } + + tempFilesList[i].Size = size; + tempFilesList[i].CompressedSize = compressedSize; + tempFilesList[i].Compression = GetFileCompressionFormat(stream, reader, tempFilesList[i].Offset); + } + + // Read FNV1a table. + + uint[] fnv1aFilesList = new uint[filesCount2]; + for (int i = 0; i < filesCount2; i++) + { + fnv1aFilesList[i] = reader.ReadUInt32BigEndian(); + } + + // Compute FNV1a of generated names path. + + uint[] fnv1aNamesList = GenerateFnv1aNamesList(namesList, filesCount1); + + // Merge all infos. + + AddToFilesList(filesCount2, fnv1aFilesList, fnv1aNamesList, namesList, tempFilesList); + } + else + { + // Read DAT info table. + + int formatByteOrder1 = reader.ReadInt32(); // LSTTCC > -3 - LLOTR > -5 / Version ? + uint filesCount1 = reader.ReadUInt32(); + + infoTableOffset = (uint)stream.Position; + uint nameInfoOffset = infoTableOffset + filesCount1 * 0x10; + + stream.Seek(nameInfoOffset, SeekOrigin.Begin); + + uint namesCount = reader.ReadUInt32(); + nameInfoOffset = (uint)stream.Position; + uint nameFieldSize = (uint)((formatByteOrder1 <= -5) ? 12 : 8); + uint namesOffset = nameInfoOffset + (namesCount * nameFieldSize); + + stream.Seek(namesOffset, SeekOrigin.Begin); + + uint namesCrcOffset = reader.ReadUInt32(); + namesOffset = (uint)stream.Position; + namesCrcOffset += (uint)stream.Position; + + stream.Seek(namesCrcOffset, SeekOrigin.Begin); + + ulong fnv1aTableOffset = (ulong)stream.Position; + + // Read FNV1a filenames table. + + uint[] fnv1aFilesList = new uint[filesCount1]; + + for (int i = 0; i < fnv1aFilesList.Length; i++) + { + fnv1aFilesList[i] = reader.ReadUInt32(); + } + + // Read files infos. + + if (formatByteOrder1 <= -2) + { + reader.ReadInt32(); + reader.ReadInt32(); + + // TODO: We should be at EOF. + if (stream.Position != stream.Length) + { + throw new IndexOutOfRangeException($"{stream.Position:x8}"); + } + } + + uint nameIndex = 0; + string fullName = ""; + string fullPath = ""; + string[] namesList = new string[filesCount1]; + string[] tempArray = new string[ushort.MaxValue]; // TODO: Find a better way to handle that. + + for (int i = 0; i < filesCount1; i++) + { + short next = 1; + string name = ""; + + do + { + stream.Seek(nameInfoOffset, SeekOrigin.Begin); + + next = reader.ReadInt16(); + short prev = reader.ReadInt16(); + int offset = reader.ReadInt32(); + + if (formatByteOrder1 <= -5) // if (nameFieldSize >= 12) + { + reader.ReadUInt32(); + } + + nameInfoOffset = (uint)stream.Position; + + if (offset > 0) + { + offset += (int)namesOffset; + + stream.Seek(offset, SeekOrigin.Begin); + + name = stream.ReadNullTerminatedString(); + } + + // NOTE: Used only for LEGO the game if you don't use the hdr file. + if (name.Length > 0) + { + if (name[0] >= 0xf0) + { + name = ""; + } + } + + if (prev != 0) + { + fullPath = tempArray[prev]; + } + + tempArray[(int)nameIndex] = fullPath; + + if (next > 0) // Folder + { + string tempName = tempArray[prev]; + + if (tempName != "") // NOTE: Long story to avoid things like 2foldername that gives problems. + { + string oldName = @"\"; // Do not use "/" + oldName += tempName; + oldName += @"\"; // Do not use "/" + } + + if (name != "") + { + fullPath += name; + fullPath += @"\"; // Do not use "/" + } + } + + nameIndex += 1; + } while (next > 0); + + fullName = fullPath; + fullName += name; + + namesList[i] = $"\\{fullName.ToLowerInvariant()}"; + } + + TempFile[] tempFilesList = new TempFile[filesCount1]; + + for (int i = 0; i < filesCount1; i++) + { + stream.Seek(infoTableOffset + i * 0x10, SeekOrigin.Begin); + + uint offsetFile = reader.ReadUInt32(); + uint compressedSize = reader.ReadUInt32(); + uint size = reader.ReadUInt32(); + byte[] packed = reader.ReadBytes(3); + byte offsetFile2 = reader.ReadByte(); + + if (formatByteOrder1 != -1) + { + offsetFile <<= 8; + } + + offsetFile += offsetFile2; + + tempFilesList[i].Offset = offsetFile; + tempFilesList[i].Size = size; + tempFilesList[i].CompressedSize = compressedSize; + tempFilesList[i].Compression = GetFileCompressionFormat(stream, reader, tempFilesList[i].Offset); + } + + // Compute FNV1a of generated names path. + + uint[] fnv1aNamesList = GenerateFnv1aNamesList(namesList, filesCount1); + + // Merge all infos. + + AddToFilesList(filesCount1, fnv1aFilesList, fnv1aNamesList, namesList, tempFilesList); + } + } + + private static uint[] GenerateFnv1aNamesList(string[] namesList, uint filesCount) + { + uint[] fnv1aNamesList = new uint[filesCount]; + + for (int i = 0; i < fnv1aNamesList.Length; i++) + { + fnv1aNamesList[i] = Fnv.Fnv1a_32_TTGames(namesList[i][1..]); + } + + return fnv1aNamesList; + } + + private static CompressionFormat GetFileCompressionFormat(FileStream stream, BinaryReader reader, ulong offset) + { + long oldPosition = stream.Position; + + stream.Seek((long)offset, SeekOrigin.Begin); + + // TODO: Found a better way to know if files are compressed/encrypted. + // The byte packed in the size field ("compressed" var) is only available for LZ2K/ZIPX. + + CompressionFormat compressionFormat = reader.ReadUInt32AsString() switch + { + "LZ2K" => CompressionFormat.LZ2K, + "ZIPX" => CompressionFormat.ZIPX, + "Defl" => CompressionFormat.Deflate_v1, + _ => CompressionFormat.None, + }; + + stream.Seek(oldPosition, SeekOrigin.Begin); + + return compressionFormat; + } + + private void AddToFilesList(uint filesCount, uint[] fnv1aFilesList, uint[] fnv1aNamesList, string[] namesList, TempFile[] tempFilesList) + { + for (int i = 0; i < filesCount; i++) + { + int j = Array.IndexOf(fnv1aFilesList, fnv1aNamesList[i]); + + Files.Add(new DATFile(namesList[i], tempFilesList[j].Offset, tempFilesList[j].Size, tempFilesList[j].CompressedSize, tempFilesList[j].Compression)); + } + } + + /// + /// Extract DATFile from the DAT archive. + /// + /// + /// DATFile object representing the file to be extracted. + /// + /// + /// Bool to decrypt/decompress the file while it's extracted or not. + /// + /// + /// Byte array representing the file data. + /// + public byte[] ExtractFile(DATFile file, bool plainData = false) + { + using FileStream stream = new(ArchiveFilePath, FileMode.Open, FileAccess.Read); + + stream.Seek((long)file.Offset, SeekOrigin.Begin); + + uint size = file.Compression == CompressionFormat.None ? file.Size : file.CompressedSize; + byte[] fileData = new byte[size]; + + stream.Read(fileData, 0, (int)size); + + if (plainData) + { + switch (file.Compression) + { + case CompressionFormat.LZ2K: + { + fileData = LZ2K.Decompress(fileData); + break; + } + + case CompressionFormat.ZIPX: + { + fileData = ZIPX.Decrypt(fileData); + break; + } + + case CompressionFormat.Deflate_v1: + { + fileData = Deflatev1.Decompress(fileData); + break; + } + } + } + + return fileData; + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/DAT/DATFile.cs b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATFile.cs new file mode 100644 index 0000000..71b7377 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DAT/DATFile.cs @@ -0,0 +1,36 @@ +namespace TTGamesExplorerRebirthLib.Formats.DAT +{ + public class DATFile + { + public string Path; + public ulong Offset; + public uint Size; + public uint CompressedSize; + public CompressionFormat Compression; + + public DATFile(string path, ulong offset, uint size, uint compressedSize, CompressionFormat compression) + { + Path = path; + Offset = offset; + Size = size; + Compression = compression; + + if (compression != CompressionFormat.None) + { + CompressedSize = compressedSize; + } + } + + public override string ToString() + { + string value = $"Path: {Path}\n\tOffset: 0x{Offset:X8}\n\tSize: 0x{Size:X8}\n"; + + if (Compression != CompressionFormat.None) + { + value += $"\tCompressedSize: 0x{CompressedSize:X8} ({Compression})\n"; + } + + return value; + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnDecoderExtensions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnDecoderExtensions.cs new file mode 100644 index 0000000..d9d8ff2 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnDecoderExtensions.cs @@ -0,0 +1,250 @@ +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.ImageSharp +{ + public static class BCnDecoderExtensions + { + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded image. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The format the encoded data is in. + /// The decoded Rgba32 image. + public static Image DecodeRawToImageRgba32(this BcDecoder decoder, Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return ColorMemoryToImage(decoder.DecodeRaw2D(inputStream, pixelWidth, pixelHeight, format)); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The array containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The format the encoded data is in. + /// The decoded Rgba32 image. + public static Image DecodeRawToImageRgba32(this BcDecoder decoder, byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return ColorMemoryToImage(decoder.DecodeRaw2D(input, pixelWidth, pixelHeight, format)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// The decoded Rgba32 image. + public static Image DecodeToImageRgba32(this BcDecoder decoder, Stream inputStream) + { + return ColorMemoryToImage(decoder.Decode2D(inputStream)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// An array of decoded Rgba32 images. + public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, Stream inputStream) + { + var decoded = decoder.DecodeAllMipMaps2D(inputStream); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The decoded Rgba32 image. + public static Image DecodeToImageRgba32(this BcDecoder decoder, KtxFile file) + { + return ColorMemoryToImage(decoder.Decode2D(file)); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// An array of decoded Rgba32 images. + public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, KtxFile file) + { + var decoded = decoder.DecodeAllMipMaps2D(file); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The decoded Rgba32 image. + public static Image DecodeToImageRgba32(this BcDecoder decoder, DdsFile file) + { + return ColorMemoryToImage(decoder.Decode2D(file)); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// An array of decoded Rgba32 images. + public static Image[] DecodeAllMipMapsToImageRgba32(this BcDecoder decoder, DdsFile file) + { + var decoded = decoder.DecodeAllMipMaps2D(file); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeRawToImageRgba32Async(this BcDecoder decoder, Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.DecodeRaw2DAsync(inputStream, pixelWidth, pixelHeight, format, token)); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The array containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeRawToImageRgba32Async(this BcDecoder decoder, byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.DecodeRaw2DAsync(input, pixelWidth, pixelHeight, format, token)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, Stream inputStream, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.Decode2DAsync(inputStream, token)); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream that contains either ktx or dds file. + /// The cancellation token for this asynchronous operation. + /// An array of decoded Rgba32 images. + public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, Stream inputStream, CancellationToken token = default) + { + var decoded = await decoder.DecodeAllMipMaps2DAsync(inputStream, token); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, KtxFile file, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.Decode2DAsync(file, token)); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// An array of decoded Rgba32 images. + public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, KtxFile file, CancellationToken token = default) + { + var decoded = await decoder.DecodeAllMipMaps2DAsync(file, token); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The decoded Rgba32 image. + public static async Task> DecodeToImageRgba32Async(this BcDecoder decoder, DdsFile file, CancellationToken token = default) + { + return ColorMemoryToImage(await decoder.Decode2DAsync(file, token)); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// An array of decoded Rgba32 images. + public static async Task[]> DecodeAllMipMapsToImageRgba32Async(this BcDecoder decoder, DdsFile file, CancellationToken token = default) + { + var decoded = await decoder.DecodeAllMipMaps2DAsync(file, token); + var output = new Image[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + output[i] = ColorMemoryToImage(decoded[i]); + } + return output; + } + + private static Image ColorMemoryToImage(Memory2D colors) + { + var output = new Image(colors.Width, colors.Height); + for (var y = 0; y < colors.Height; y++) + { + var yPixels = output.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var yColors = colors.Span.GetRowSpan(y); + + MemoryMarshal.Cast(yColors).CopyTo(yPixels); + } + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnEncoderExtensions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnEncoderExtensions.cs new file mode 100644 index 0000000..5b03a90 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net.ImageSharp/BCnEncoderExtensions.cs @@ -0,0 +1,315 @@ +using CommunityToolkit.HighPerformance; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.ImageSharp +{ + public static class BCnEncoderExtensions + { + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The image to encode. + /// The stream to write the encoded image to. + public static void EncodeToStream(this BcEncoder encoder, Image inputImage, Stream outputStream) + { + encoder.EncodeToStream(ImageToMemory2D(inputImage), outputStream); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The image to encode. + /// The Ktx file containing the encoded image. + public static KtxFile EncodeToKtx(this BcEncoder encoder, Image inputImage) + { + return encoder.EncodeToKtx(ImageToMemory2D(inputImage)); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The image to encode. + /// The Dds file containing the encoded image. + public static DdsFile EncodeToDds(this BcEncoder encoder, Image inputImage) + { + return encoder.EncodeToDds(ImageToMemory2D(inputImage)); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// A list of raw encoded data. + public static byte[][] EncodeToRawBytes(this BcEncoder encoder, Image inputImage) + { + return encoder.EncodeToRawBytes(ImageToMemory2D(inputImage)); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// The raw encoded data. + public static byte[] EncodeToRawBytes(this BcEncoder encoder, Image inputImage, int mipLevel, out int mipWidth, out int mipHeight) + { + return encoder.EncodeToRawBytes(ImageToMemory2D(inputImage), mipLevel, out mipWidth, out mipHeight); + } + + /// + /// Encodes all cubemap faces and mipmap levels into either a ktx or a dds file and writes it to the output stream. + /// Order is +X, -X, +Y, -Y, +Z, -Z + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The stream to write the encoded image to. + public static void EncodeCubeMapToStream(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front, Stream outputStream) + { + encoder.EncodeCubeMapToStream( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + outputStream + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Ktx file. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The Ktx file containing the encoded image. + public static KtxFile EncodeCubeMapToKtx(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front) + { + return encoder.EncodeCubeMapToKtx( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front) + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Dds file. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The Dds file containing the encoded image. + public static DdsFile EncodeCubeMapToDds(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front) + { + return encoder.EncodeCubeMapToDds( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front) + ); + } + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The image to encode. + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + public static Task EncodeToStreamAsync(this BcEncoder encoder, Image inputImage, Stream outputStream, CancellationToken token = default) + { + return encoder.EncodeToStreamAsync(ImageToMemory2D(inputImage), outputStream, token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The image to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Ktx file containing the encoded image. + public static Task EncodeToKtxAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default) + { + return encoder.EncodeToKtxAsync(ImageToMemory2D(inputImage), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The image to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Dds file containing the encoded image. + public static Task EncodeToDdsAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default) + { + return encoder.EncodeToDdsAsync(ImageToMemory2D(inputImage), token); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// A list of raw encoded data. + public static Task EncodeToRawBytesAsync(this BcEncoder encoder, Image inputImage, CancellationToken token = default) + { + return encoder.EncodeToRawBytesAsync(ImageToMemory2D(inputImage), token); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded data. + /// + /// The image to encode. + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The raw encoded data. + /// To get the width and height of the encoded mipLevel, see . + public static Task EncodeToRawBytesAsync(this BcEncoder encoder, Image inputImage, int mipLevel, CancellationToken token = default) + { + return encoder.EncodeToRawBytesAsync(ImageToMemory2D(inputImage), mipLevel, token); + } + + /// + /// Encodes all cubemap faces and mipmap levels into either a ktx or a dds file and writes it to the output stream asynchronously. + /// Order is +X, -X, +Y, -Y, +Z, -Z + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + public static Task EncodeCubeMapToStreamAsync(this BcEncoder encoder, Image right, Image left, Image top, Image down, + Image back, Image front, Stream outputStream, CancellationToken token = default) + { + return encoder.EncodeCubeMapToStreamAsync( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + outputStream, + token + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Ktx file asynchronously. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Ktx file containing the encoded image. + public static Task EncodeCubeMapToKtxAsync(this BcEncoder encoder, Image right, Image left, Image top, + Image down, Image back, Image front, CancellationToken token = default) + { + return encoder.EncodeCubeMapToKtxAsync( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + token + ); + } + + /// + /// Encodes all cubemap faces and mipmap levels into a Dds file asynchronously. + /// Order is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The right face of the cubemap. + /// The left face of the cubemap. + /// The top face of the cubemap. + /// The bottom face of the cubemap. + /// The back face of the cubemap. + /// The front face of the cubemap. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The Dds file containing the encoded image. + public static Task EncodeCubeMapToDdsAsync(this BcEncoder encoder, Image right, Image left, Image top, + Image down, Image back, Image front, CancellationToken token = default) + { + return encoder.EncodeCubeMapToDdsAsync( + ImageToMemory2D(right), + ImageToMemory2D(left), + ImageToMemory2D(top), + ImageToMemory2D(down), + ImageToMemory2D(back), + ImageToMemory2D(front), + token + ); + } + + /// + /// Calculates the number of mipmap levels that will be generated for the given input image. + /// + /// The image to use for the calculation. + /// The number of mipmap levels that will be generated for the input image. + public static int CalculateNumberOfMipLevels(this BcEncoder encoder, Image inputImage) + { + return encoder.CalculateNumberOfMipLevels(inputImage.Width, inputImage.Height); + } + + /// + /// Calculates the size of a given mipmap level. + /// + /// The image to use for the calculation. + /// The mipLevel to calculate (0 is original image) + /// The mipmap width calculated + /// The mipmap height calculated + /* + public static void CalculateMipMapSize(this BcEncoder encoder, Image inputImage, int mipLevel, out int mipWidth, out int mipHeight) + { + BcEncoder.CalculateMipMapSize(inputImage.Width, inputImage.Height, mipLevel, out mipWidth, out mipHeight); + } + */ + + private static Memory2D ImageToMemory2D(Image inputImage) + { + _ = inputImage.GetPixelMemoryGroup()[0]; + var colors = new ColorRgba32[inputImage.Width * inputImage.Height]; + for (var y = 0; y < inputImage.Height; y++) + { + var yPixels = inputImage.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + var yColors = colors.AsSpan(y * inputImage.Width, inputImage.Width); + + MemoryMarshal.Cast(yPixels).CopyTo(yColors); + } + var memory = colors.AsMemory().AsMemory2D(inputImage.Height, inputImage.Width); + return memory; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcBlockDecoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcBlockDecoder.cs new file mode 100644 index 0000000..1c576ac --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcBlockDecoder.cs @@ -0,0 +1,186 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder +{ + internal interface IBcBlockDecoder where T : unmanaged + { + T[] Decode(ReadOnlyMemory data, OperationContext context); + T DecodeBlock(ReadOnlySpan data); + } + + internal abstract class BaseBcBlockDecoder : IBcBlockDecoder where T : unmanaged where TBlock : unmanaged + { + private static readonly object lockObj = new object(); + + public TBlock[] Decode(ReadOnlyMemory data, OperationContext context) + { + + if (data.Length % Unsafe.SizeOf() != 0) + { + throw new InvalidDataException("Given data does not align with the block length."); + } + + var blockCount = data.Length / Unsafe.SizeOf(); + var output = new TBlock[blockCount]; + + var currentBlocks = 0; + if (context.IsParallel) + { + var options = new ParallelOptions + { + CancellationToken = context.CancellationToken, + MaxDegreeOfParallelism = context.TaskCount + }; + Parallel.For(0, blockCount, options, i => + { + var encodedBlocks = MemoryMarshal.Cast(data.Span); + output[i] = DecodeBlock(encodedBlocks[i]); + + if (context.Progress != null) + { + lock (lockObj) + { + context.Progress.Report(++currentBlocks); + } + } + }); + } + else + { + var encodedBlocks = MemoryMarshal.Cast(data.Span); + for (var i = 0; i < blockCount; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i] = DecodeBlock(encodedBlocks[i]); + + context.Progress?.Report(++currentBlocks); + } + } + + return output; + } + + public TBlock DecodeBlock(ReadOnlySpan data) + { + var encodedBlock = MemoryMarshal.Cast(data)[0]; + return DecodeBlock(encodedBlock); + } + + protected abstract TBlock DecodeBlock(T block); + } + + internal class Bc1NoAlphaDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc1Block block) + { + return block.Decode(false); + } + } + + internal class Bc1ADecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc1Block block) + { + return block.Decode(true); + } + } + + internal class Bc2Decoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc2Block block) + { + return block.Decode(); + } + } + + internal class Bc3Decoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc3Block block) + { + return block.Decode(); + } + } + + internal class Bc4Decoder : BaseBcBlockDecoder + { + private readonly ColorComponent component; + + public Bc4Decoder(ColorComponent component) + { + this.component = component; + } + + protected override RawBlock4X4Rgba32 DecodeBlock(Bc4Block block) + { + return block.Decode(component); + } + } + + internal class Bc5Decoder : BaseBcBlockDecoder + { + private readonly ColorComponent component1; + private readonly ColorComponent component2; + + public Bc5Decoder(ColorComponent component1, ColorComponent component2) + { + this.component1 = component1; + this.component2 = component2; + } + + protected override RawBlock4X4Rgba32 DecodeBlock(Bc5Block block) + { + return block.Decode(component1, component2); + } + } + + internal class Bc6UDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4RgbFloat DecodeBlock(Bc6Block block) + { + return block.Decode(false); + } + } + + internal class Bc6SDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4RgbFloat DecodeBlock(Bc6Block block) + { + return block.Decode(true); + } + } + + internal class Bc7Decoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(Bc7Block block) + { + return block.Decode(); + } + } + + internal class AtcDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(AtcBlock block) + { + return block.Decode(); + } + } + + internal class AtcExplicitAlphaDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(AtcExplicitAlphaBlock block) + { + return block.Decode(); + } + } + + internal class AtcInterpolatedAlphaDecoder : BaseBcBlockDecoder + { + protected override RawBlock4X4Rgba32 DecodeBlock(AtcInterpolatedAlphaBlock block) + { + return block.Decode(); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcDecoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcDecoder.cs new file mode 100644 index 0000000..7e4904d --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/BcDecoder.cs @@ -0,0 +1,1824 @@ +using CommunityToolkit.HighPerformance; +using System.Runtime.CompilerServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder +{ + /// + /// Decodes compressed files into Rgba Format. + /// + public class BcDecoder + { + /// + /// The input options of the decoder. + /// + public DecoderInputOptions InputOptions { get; } = new DecoderInputOptions(); + + /// + /// The options for the decoder. + /// + public DecoderOptions Options { get; } = new DecoderOptions(); + + /// + /// The output options of the decoder. + /// + public DecoderOutputOptions OutputOptions { get; } = new DecoderOutputOptions(); + #region LDR + #region Async Api + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawAsync(Stream inputStream, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternal(dataArray, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawAsync(ReadOnlyMemory input, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternal(input, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, true, token), token); + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, true, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the raw encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRaw2DAsync(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternal(dataArray, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRaw2DAsync(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternal(input, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> Decode2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternal2D(inputStream, false, token)[0], token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMaps2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternal2D(inputStream, false, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> Decode2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth), token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMaps2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternal(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> Decode2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternal(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth), token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMaps2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternal(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + #endregion + + #region Sync API + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgba32[] DecodeRaw(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return DecodeRaw(dataArray, pixelWidth, pixelHeight, format); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgba32[] DecodeRaw(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return DecodeRawInternal(input, pixelWidth, pixelHeight, format, default); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The decoded image. + public ColorRgba32[] Decode(KtxFile file) + { + return DecodeInternal(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public ColorRgba32[][] DecodeAllMipMaps(KtxFile file) + { + return DecodeInternal(file, true, default); + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The decoded image. + public ColorRgba32[] Decode(DdsFile file) + { + return DecodeInternal(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// An array of decoded images. + public ColorRgba32[][] DecodeAllMipMaps(DdsFile file) + { + return DecodeInternal(file, true, default); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// + /// The stream containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRaw2D(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + var decoded = DecodeRaw(dataArray, pixelWidth, pixelHeight, format); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Decode a single encoded image from raw bytes. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRaw2D(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var decoded = DecodeRawInternal(input, pixelWidth, pixelHeight, format, default); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// The decoded image. + public Memory2D Decode2D(Stream inputStream) + { + return DecodeFromStreamInternal2D(inputStream, false, default)[0]; + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// + /// The stream containing a Ktx or Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMaps2D(Stream inputStream) + { + return DecodeFromStreamInternal2D(inputStream, true, default); + } + + /// + /// Decode the main image from a Ktx file. + /// + /// The loaded Ktx file. + /// The decoded image. + public Memory2D Decode2D(KtxFile file) + { + return DecodeInternal(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMaps2D(KtxFile file) + { + var decoded = DecodeInternal(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode the main image from a Dds file. + /// + /// The loaded Dds file. + /// The decoded image. + public Memory2D Decode2D(DdsFile file) + { + return DecodeInternal(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// + /// The loaded Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMaps2D(DdsFile file) + { + var decoded = DecodeInternal(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode a single block from raw bytes and return it as a . + /// Input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// + /// The encoded block in bytes. + /// The compression format used. + /// The decoded 4x4 block. + public Memory2D DecodeBlock(ReadOnlySpan blockData, CompressionFormat format) + { + var output = new ColorRgba32[4, 4]; + DecodeBlockInternal(blockData, format, output); + return output; + } + + /// + /// Decode a single block from raw bytes and write it to the given output span. + /// Output span size must be exactly 4x4 and input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// + /// The encoded block in bytes. + /// The compression format used. + /// The destination span of the decoded data. + public void DecodeBlock(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + DecodeBlockInternal(blockData, format, outputSpan); + } + + /// + /// Decode a single block from a stream and write it to the given output span. + /// Output span size must be exactly 4x4. + /// + /// The stream to read encoded blocks from. + /// The compression format used. + /// The destination span of the decoded data. + /// The number of bytes read from the stream. Zero (0) if reached the end of stream. + public int DecodeBlock(Stream inputStream, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + + Span input = stackalloc byte[16]; + input = input[..GetBlockSize(format)]; + + var bytesRead = inputStream.Read(input); + + if (bytesRead == 0) + { + return 0; //End of stream + } + + if (bytesRead != input.Length) + { + throw new Exception("Input stream does not have enough data available for a full block."); + } + + DecodeBlockInternal(input, format, outputSpan); + return bytesRead; + } + + /// + /// Check whether a file is encoded in a supported format. + /// + /// The loaded ktx file to check + /// If the format of the file is one of the supported formats. + public static bool IsSupportedFormat(KtxFile file) + { + return GetCompressionFormat(file.header.GlInternalFormat) != CompressionFormat.Unknown; + } + + /// + /// Check whether a file is encoded in a supported format. + /// + /// The loaded dds file to check + /// If the format of the file is one of the supported formats. + public bool IsSupportedFormat(DdsFile file) + { + return GetCompressionFormat(file) != CompressionFormat.Unknown; + } + + /// + /// Gets the format of the file. + /// + /// The loaded ktx file to check + /// The of the file. + public static CompressionFormat GetFormat(KtxFile file) + { + return GetCompressionFormat(file.header.GlInternalFormat); + } + + /// + /// Gets the format of the file. + /// + /// The loaded dds file to check + /// The of the file. + public CompressionFormat GetFormat(DdsFile file) + { + return GetCompressionFormat(file); + } + + + #endregion + #endregion + + #region HDR + #region Async Api + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawHdrAsync(Stream inputStream, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternalHdr(dataArray, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeRawHdrAsync(ReadOnlyMemory input, CompressionFormat format, int pixelWidth, int pixelHeight, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeHdrAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsHdrAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, true, token), token); + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeHdrAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0], token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task DecodeAllMipMapsHdrAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, true, token), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the raw encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRawHdr2DAsync(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return Task.Run(() => DecodeRawInternalHdr(dataArray, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The containing the encoded data. + /// The Format the encoded data is in. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeRawHdr2DAsync(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token = default) + { + return Task.Run(() => DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, token) + .AsMemory().AsMemory2D(pixelHeight, pixelWidth), token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeHdr2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, false, token)[0], token); + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMapsHdr2DAsync(Stream inputStream, CancellationToken token = default) + { + return Task.Run(() => DecodeFromStreamInternalHdr2D(inputStream, false, token), token); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeHdr2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth), token); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMapsHdr2DAsync(KtxFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternalHdr(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task> DecodeHdr2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => DecodeInternalHdr(file, false, token)[0] + .AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth), token); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The cancellation token for this asynchronous operation. + /// The awaitable operation to retrieve the decoded image. + public Task[]> DecodeAllMipMapsHdr2DAsync(DdsFile file, CancellationToken token = default) + { + return Task.Run(() => + { + var decoded = DecodeInternalHdr(file, true, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + }, token); + } + + #endregion + + #region Sync API + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgbFloat[] DecodeRawHdr(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + return DecodeRawHdr(dataArray, pixelWidth, pixelHeight, format); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public ColorRgbFloat[] DecodeRawHdr(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + return DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, default); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The decoded image. + public ColorRgbFloat[] DecodeHdr(KtxFile file) + { + return DecodeInternalHdr(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public ColorRgbFloat[][] DecodeAllMipMapsHdr(KtxFile file) + { + return DecodeInternalHdr(file, true, default); + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The decoded image. + public ColorRgbFloat[] DecodeHdr(DdsFile file) + { + return DecodeInternalHdr(file, false, default)[0]; + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// An array of decoded images. + public ColorRgbFloat[][] DecodeAllMipMapsHdr(DdsFile file) + { + return DecodeInternalHdr(file, true, default); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method will read the expected amount of bytes from the given input stream and decode it. + /// Make sure there is no file header information left in the stream before the encoded data. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing the encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRawHdr2D(Stream inputStream, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var dataArray = new byte[GetBufferSize(format, pixelWidth, pixelHeight)]; + inputStream.Read(dataArray, 0, dataArray.Length); + + var decoded = DecodeRawHdr(dataArray, pixelWidth, pixelHeight, format); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Decode a single encoded image from raw bytes. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The byte array containing the raw encoded data. + /// The pixelWidth of the image. + /// The pixelHeight of the image. + /// The Format the encoded data is in. + /// The decoded image. + public Memory2D DecodeRawHdr2D(byte[] input, int pixelWidth, int pixelHeight, CompressionFormat format) + { + var decoded = DecodeRawInternalHdr(input, pixelWidth, pixelHeight, format, default); + return decoded.AsMemory().AsMemory2D(pixelHeight, pixelWidth); + } + + /// + /// Read a Ktx or Dds file from a stream and decode the main image from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// The decoded image. + public Memory2D DecodeHdr2D(Stream inputStream) + { + return DecodeFromStreamInternalHdr2D(inputStream, false, default)[0]; + } + + /// + /// Read a Ktx or Dds file from a stream and decode all available mipmaps from it. + /// The type of file will be detected automatically. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream containing a Ktx or Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMapsHdr2D(Stream inputStream) + { + return DecodeFromStreamInternalHdr2D(inputStream, true, default); + } + + /// + /// Decode the main image from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// The decoded image. + public Memory2D DecodeHdr2D(KtxFile file) + { + return DecodeInternalHdr(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.PixelHeight, (int)file.header.PixelWidth); + } + + /// + /// Decode all available mipmaps from a Ktx file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Ktx file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMapsHdr2D(KtxFile file) + { + var decoded = DecodeInternalHdr(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode the main image from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// The decoded image. + public Memory2D DecodeHdr2D(DdsFile file) + { + return DecodeInternalHdr(file, false, default)[0].AsMemory().AsMemory2D((int)file.header.dwHeight, (int)file.header.dwWidth); + } + + /// + /// Decode all available mipmaps from a Dds file. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The loaded Dds file. + /// An array of decoded images. + public Memory2D[] DecodeAllMipMapsHdr2D(DdsFile file) + { + var decoded = DecodeInternalHdr(file, true, default); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + return mem2Ds; + } + + /// + /// Decode a single block from raw bytes and return it as a . + /// Input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The encoded block in bytes. + /// The compression format used. + /// The decoded 4x4 block. + public static Memory2D DecodeBlockHdr(ReadOnlySpan blockData, CompressionFormat format) + { + var output = new ColorRgbFloat[4, 4]; + DecodeBlockInternalHdr(blockData, format, output); + return output; + } + + /// + /// Decode a single block from raw bytes and write it to the given output span. + /// Output span size must be exactly 4x4 and input Span size needs to equal the block size. + /// To get the block size (in bytes) of the compression format used, see . + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The encoded block in bytes. + /// The compression format used. + /// The destination span of the decoded data. + public static void DecodeBlockHdr(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + DecodeBlockInternalHdr(blockData, format, outputSpan); + } + + /// + /// Decode a single block from a stream and write it to the given output span. + /// Output span size must be exactly 4x4. + /// This method is only for compressed Hdr formats. Please use the non-Hdr methods for other formats. + /// + /// The stream to read encoded blocks from. + /// The compression format used. + /// The destination span of the decoded data. + /// The number of bytes read from the stream. Zero (0) if reached the end of stream. + public int DecodeBlockHdr(Stream inputStream, CompressionFormat format, Span2D outputSpan) + { + if (outputSpan.Width != 4 || outputSpan.Height != 4) + { + throw new ArgumentException($"Single block decoding needs an output span of exactly 4x4"); + } + + Span input = stackalloc byte[16]; + input = input[..GetBlockSize(format)]; + + var bytesRead = inputStream.Read(input); + + if (bytesRead == 0) + { + return 0; //End of stream + } + + if (bytesRead != input.Length) + { + throw new Exception("Input stream does not have enough data available for a full block."); + } + + DecodeBlockInternalHdr(input, format, outputSpan); + return bytesRead; + } + + /// + /// Check whether a file is encoded in a supported HDR format. + /// + /// The loaded ktx file to check + /// If the format of the file is one of the supported HDR formats. + public static bool IsHdrFormat(KtxFile file) + { + return GetCompressionFormat(file.header.GlInternalFormat).IsHdrFormat(); + } + + /// + /// Check whether a file is encoded in a supported HDR format. + /// + /// The loaded dds file to check + /// If the format of the file is one of the supported HDR formats. + public bool IsHdrFormat(DdsFile file) + { + return GetCompressionFormat(file).IsHdrFormat(); + } + + #endregion + #endregion + /// + /// Load a stream and extract either the main image or all mip maps. + /// + /// The stream containing the image file. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private Memory2D[] DecodeFromStreamInternal2D(Stream stream, bool allMipMaps, CancellationToken token) + { + var format = ImageFile.DetermineImageFormat(stream); + + switch (format) + { + case ImageFileFormat.Dds: + { + var file = DdsFile.Load(stream); + var decoded = DecodeInternal(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + case ImageFileFormat.Ktx: + { + var file = KtxFile.Load(stream); + var decoded = DecodeInternal(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + default: + throw new InvalidOperationException("Unknown image format."); + } + } + + /// + /// Load a KTX file and extract either the main image or all mip maps. + /// + /// The Ktx file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgba32[][] DecodeInternal(KtxFile file, bool allMipMaps, CancellationToken token) + { + var mipMaps = allMipMaps ? file.MipMaps.Count : 1; + var colors = new ColorRgba32[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file.header.GlInternalFormat); + var totalBlocks = file.MipMaps.Take(mipMaps).Sum(m => m.Faces[0].Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + if (IsSupportedRawFormat(file.header.GlInternalFormat)) + { + var decoder = GetRawDecoder(file.header.GlInternalFormat); + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.MipMaps[mip].Faces[0].Data; + + colors[mip] = decoder.Decode(data, context); + + context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize)); + } + } + else + { + var decoder = GetRgba32Decoder(file.header.GlInternalFormat); + var format = GetCompressionFormat(file.header.GlInternalFormat); + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {file.header.GlInternalFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.MipMaps[mip].Faces[0].Data; + var pixelWidth = file.MipMaps[mip].Width; + var pixelHeight = file.MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + colors[mip] = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize)); + } + } + + return colors; + } + + /// + /// Load a DDS file and extract either the main image or all mip maps. + /// + /// The Dds file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgba32[][] DecodeInternal(DdsFile file, bool allMipMaps, CancellationToken token) + { + var mipMaps = allMipMaps ? Math.Max(1, file.header.dwMipMapCount) : 1; + var colors = new ColorRgba32[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file); + var totalBlocks = file.Faces[0].MipMaps.Take((int)mipMaps).Sum(m => m.Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + if (IsSupportedRawFormat(file)) + { + var decoder = GetRawDecoder(file); + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.Faces[0].MipMaps[mip].Data; + + colors[mip] = decoder.Decode(data, context); + + context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize)); + } + } + else + { + var dxtFormat = file.header.ddsPixelFormat.IsDxt10Format + ? file.dx10Header.dxgiFormat + : file.header.ddsPixelFormat.DxgiFormat; + var format = GetCompressionFormat(file); + var decoder = GetRgba32Decoder(format); + + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {dxtFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.Faces[0].MipMaps[mip].Data; + var pixelWidth = file.Faces[0].MipMaps[mip].Width; + var pixelHeight = file.Faces[0].MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + var image = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + colors[mip] = image; + + context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize)); + } + } + + return colors; + } + + /// + /// Decode raw encoded image asynchronously. + /// + /// The containing the encoded data. + /// The width of the image. + /// The height of the image. + /// The Format the encoded data is in. + /// The cancellation token for this operation. May be default, if the operation is not asynchronous. + /// The decoded Rgba32 image. + private ColorRgba32[] DecodeRawInternal(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token) + { + if (input.Length % GetBlockSize(format) != 0) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(format); + var totalBlocks = input.Length / blockSize; + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var isCompressedFormat = format.IsCompressedFormat(); + if (isCompressedFormat) + { + // DecodeInternal as compressed data + var decoder = GetRgba32Decoder(format); + + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + + var blocks = decoder.Decode(input, context); + + return ImageToBlocks.ColorsFromRawBlocks(blocks, pixelWidth, pixelHeight); ; + } + + // DecodeInternal as raw data + var rawDecoder = GetRawDecoder(format); + + return rawDecoder.Decode(input, context); + } + + private void DecodeBlockInternal(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + var decoder = GetRgba32Decoder(format); + if (format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an RGBA32 compatible format: {format}, please use the HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + if (blockData.Length != GetBlockSize(format)) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var rawBlock = decoder.DecodeBlock(blockData); + var pixels = rawBlock.AsSpan; + + pixels[..4].CopyTo(outputSpan.GetRowSpan(0)); + pixels.Slice(4, 4).CopyTo(outputSpan.GetRowSpan(1)); + pixels.Slice(8, 4).CopyTo(outputSpan.GetRowSpan(2)); + pixels.Slice(12, 4).CopyTo(outputSpan.GetRowSpan(3)); + } + + #region Hdr internals + + /// + /// Load a stream and extract either the main image or all mip maps. + /// + /// The stream containing the image file. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private Memory2D[] DecodeFromStreamInternalHdr2D(Stream stream, bool allMipMaps, CancellationToken token) + { + var format = ImageFile.DetermineImageFormat(stream); + + switch (format) + { + case ImageFileFormat.Dds: + { + var file = DdsFile.Load(stream); + var decoded = DecodeInternalHdr(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.Faces[0].MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + case ImageFileFormat.Ktx: + { + var file = KtxFile.Load(stream); + var decoded = DecodeInternalHdr(file, allMipMaps, token); + var mem2Ds = new Memory2D[decoded.Length]; + for (var i = 0; i < decoded.Length; i++) + { + var mip = file.MipMaps[i]; + mem2Ds[i] = decoded[i].AsMemory().AsMemory2D((int)mip.Height, (int)mip.Width); + } + + return mem2Ds; + } + + default: + throw new InvalidOperationException("Unknown image format."); + } + } + + /// + /// Load a KTX file and extract either the main image or all mip maps. + /// + /// The Ktx file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgbFloat[][] DecodeInternalHdr(KtxFile file, bool allMipMaps, CancellationToken token) + { + var mipMaps = allMipMaps ? file.MipMaps.Count : 1; + var colors = new ColorRgbFloat[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file.header.GlInternalFormat); + var totalBlocks = file.MipMaps.Take(mipMaps).Sum(m => m.Faces[0].Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var decoder = GetRgbFloatDecoder(file.header.GlInternalFormat); + var format = GetCompressionFormat(file.header.GlInternalFormat); + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {file.header.GlInternalFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.MipMaps[mip].Faces[0].Data; + var pixelWidth = file.MipMaps[mip].Width; + var pixelHeight = file.MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + colors[mip] = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + context.Progress.SetProcessedBlocks(file.MipMaps.Take(mip + 1).Sum(x => x.Faces[0].Data.Length / blockSize)); + } + + return colors; + } + + /// + /// Load a DDS file and extract either the main image or all mip maps. + /// + /// The Dds file to decode. + /// If all mip maps or only the main image should be decoded. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// An array of decoded Rgba32 images. + private ColorRgbFloat[][] DecodeInternalHdr(DdsFile file, bool allMipMaps, CancellationToken token) + { + var mipMaps = allMipMaps ? file.header.dwMipMapCount : 1; + var colors = new ColorRgbFloat[mipMaps][]; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(file); + var totalBlocks = file.Faces[0].MipMaps.Take((int)mipMaps).Sum(m => m.Data.Length / blockSize); + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var dxtFormat = file.header.ddsPixelFormat.IsDxt10Format + ? file.dx10Header.dxgiFormat + : file.header.ddsPixelFormat.DxgiFormat; + var format = GetCompressionFormat(file); + var decoder = GetRgbFloatDecoder(format); + + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {dxtFormat}"); + } + + for (var mip = 0; mip < mipMaps; mip++) + { + var data = file.Faces[0].MipMaps[mip].Data; + var pixelWidth = file.Faces[0].MipMaps[mip].Width; + var pixelHeight = file.Faces[0].MipMaps[mip].Height; + + var blocks = decoder.Decode(data, context); + + var image = ImageToBlocks.ColorsFromRawBlocks(blocks, (int)pixelWidth, (int)pixelHeight); + + colors[mip] = image; + + context.Progress.SetProcessedBlocks(file.Faces[0].MipMaps.Take(mip + 1).Sum(x => x.Data.Length / blockSize)); + } + + return colors; + } + + /// + /// Decode raw encoded image asynchronously. + /// + /// The containing the encoded data. + /// The width of the image. + /// The height of the image. + /// The Format the encoded data is in. + /// The cancellation token for this operation. May be default, if the operation is not asynchronous. + /// The decoded Rgba32 image. + private ColorRgbFloat[] DecodeRawInternalHdr(ReadOnlyMemory input, int pixelWidth, int pixelHeight, CompressionFormat format, CancellationToken token) + { + if (input.Length % GetBlockSize(format) != 0) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var blockSize = GetBlockSize(format); + var totalBlocks = input.Length / blockSize; + + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + var decoder = GetRgbFloatDecoder(format); + + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + + var blocks = decoder.Decode(input, context); + + return ImageToBlocks.ColorsFromRawBlocks(blocks, pixelWidth, pixelHeight); + } + + private static void DecodeBlockInternalHdr(ReadOnlySpan blockData, CompressionFormat format, Span2D outputSpan) + { + var decoder = GetRgbFloatDecoder(format); + if (!format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not an HDR format: {format}, please use the non-HDR versions of the decode methods."); + } + if (decoder == null) + { + throw new NotSupportedException($"This Format is not supported: {format}"); + } + if (blockData.Length != GetBlockSize(format)) + { + throw new ArgumentException("The size of the input buffer does not align with the compression format."); + } + + var rawBlock = decoder.DecodeBlock(blockData); + var pixels = rawBlock.AsSpan; + + pixels[..4].CopyTo(outputSpan.GetRowSpan(0)); + pixels.Slice(4, 4).CopyTo(outputSpan.GetRowSpan(1)); + pixels.Slice(8, 4).CopyTo(outputSpan.GetRowSpan(2)); + pixels.Slice(12, 4).CopyTo(outputSpan.GetRowSpan(3)); + } + #endregion + + #region Support + + #region Is supported format + + private static bool IsSupportedRawFormat(GlInternalFormat format) + { + return IsSupportedRawFormat(GetCompressionFormat(format)); + } + + private bool IsSupportedRawFormat(DdsFile file) + { + return IsSupportedRawFormat(GetCompressionFormat(file)); + } + + private static bool IsSupportedRawFormat(CompressionFormat format) + { + return format switch + { + CompressionFormat.R or CompressionFormat.Rg or CompressionFormat.Rgb or CompressionFormat.Rgba or CompressionFormat.Bgra => true, + _ => false, + }; + } + + #endregion + + #region Get decoder + + private IBcBlockDecoder GetRgba32Decoder(GlInternalFormat format) + { + return GetRgba32Decoder(GetCompressionFormat(format)); + } + + private IBcBlockDecoder GetRgba32Decoder(DdsFile file) + { + return GetRgba32Decoder(GetCompressionFormat(file)); + } + + private IBcBlockDecoder GetRgba32Decoder(CompressionFormat format) + { + return format switch + { + CompressionFormat.Bc1 => new Bc1NoAlphaDecoder(), + CompressionFormat.Bc1WithAlpha => new Bc1ADecoder(), + CompressionFormat.Bc2 => new Bc2Decoder(), + CompressionFormat.Bc3 => new Bc3Decoder(), + CompressionFormat.Bc4 => new Bc4Decoder(OutputOptions.Bc4Component), + CompressionFormat.Bc5 => new Bc5Decoder(OutputOptions.Bc5Component1, OutputOptions.Bc5Component2), + CompressionFormat.Bc7 => new Bc7Decoder(), + CompressionFormat.Atc => new AtcDecoder(), + CompressionFormat.AtcExplicitAlpha => new AtcExplicitAlphaDecoder(), + CompressionFormat.AtcInterpolatedAlpha => new AtcInterpolatedAlphaDecoder(), + _ => null, + }; + } + + private static IBcBlockDecoder GetRgbFloatDecoder(GlInternalFormat format) + { + return GetRgbFloatDecoder(GetCompressionFormat(format)); + } + + private IBcBlockDecoder GetRgbFloatDecoder(DdsFile file) + { + return GetRgbFloatDecoder(GetCompressionFormat(file)); + } + + private static IBcBlockDecoder GetRgbFloatDecoder(CompressionFormat format) + { + return format switch + { + CompressionFormat.Bc6S => new Bc6SDecoder(), + CompressionFormat.Bc6U => new Bc6UDecoder(), + _ => null, + }; + } + + #endregion + + #region Get raw decoder + + private IRawDecoder GetRawDecoder(GlInternalFormat format) + { + return GetRawDecoder(GetCompressionFormat(format)); + } + + private IRawDecoder GetRawDecoder(DdsFile file) + { + return GetRawDecoder(GetCompressionFormat(file)); + } + + private IRawDecoder GetRawDecoder(CompressionFormat format) + { + return format switch + { + CompressionFormat.R => new RawRDecoder(OutputOptions.RedAsLuminance), + CompressionFormat.Rg => new RawRgDecoder(), + CompressionFormat.Rgb => new RawRgbDecoder(), + CompressionFormat.Rgba => new RawRgbaDecoder(), + CompressionFormat.Bgra => new RawBgraDecoder(), + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), + }; + } + + #endregion + + #region Get block size + + /// + /// Gets the number of total blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The total number of blocks. + public static int GetBlockCount(int pixelWidth, int pixelHeight) + { + return ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight); + } + + /// + /// Gets the number of blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The amount of blocks in the x-axis + /// The amount of blocks in the y-axis + public static void GetBlockCount(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight) + { + ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight, out blocksWidth, out blocksHeight); + } + + private static int GetBlockSize(GlInternalFormat format) + { + return GetBlockSize(GetCompressionFormat(format)); + } + + private int GetBlockSize(DdsFile file) + { + return GetBlockSize(GetCompressionFormat(file)); + } + + /// + /// Get the size of blocks for the given compression format in bytes. + /// + /// The compression format used. + /// The size of a single block in bytes. + public static int GetBlockSize(CompressionFormat format) + { + return format switch + { + CompressionFormat.R => 1, + CompressionFormat.Rg => 2, + CompressionFormat.Rgb => 3, + CompressionFormat.Rgba => 4, + CompressionFormat.Bgra => 4, + CompressionFormat.Bc1 or CompressionFormat.Bc1WithAlpha => Unsafe.SizeOf(), + CompressionFormat.Bc2 => Unsafe.SizeOf(), + CompressionFormat.Bc3 => Unsafe.SizeOf(), + CompressionFormat.Bc4 => Unsafe.SizeOf(), + CompressionFormat.Bc5 => Unsafe.SizeOf(), + CompressionFormat.Bc6S or CompressionFormat.Bc6U => Unsafe.SizeOf(), + CompressionFormat.Bc7 => Unsafe.SizeOf(), + CompressionFormat.Atc => Unsafe.SizeOf(), + CompressionFormat.AtcExplicitAlpha => Unsafe.SizeOf(), + CompressionFormat.AtcInterpolatedAlpha => Unsafe.SizeOf(), + CompressionFormat.Unknown => 0, + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), + }; + } + + #endregion + + private static CompressionFormat GetCompressionFormat(GlInternalFormat format) + { + return format switch + { + GlInternalFormat.GlR8 => CompressionFormat.R, + GlInternalFormat.GlRg8 => CompressionFormat.Rg, + GlInternalFormat.GlRgb8 => CompressionFormat.Rgb, + GlInternalFormat.GlRgba8 => CompressionFormat.Rgba, + // HINT: Bgra is not supported by default. The format enum is added by an extension by Apple. + GlInternalFormat.GlBgra8Extension => CompressionFormat.Bgra, + GlInternalFormat.GlCompressedRgbS3TcDxt1Ext => CompressionFormat.Bc1, + GlInternalFormat.GlCompressedRgbaS3TcDxt1Ext => CompressionFormat.Bc1WithAlpha, + GlInternalFormat.GlCompressedRgbaS3TcDxt3Ext => CompressionFormat.Bc2, + GlInternalFormat.GlCompressedRgbaS3TcDxt5Ext => CompressionFormat.Bc3, + GlInternalFormat.GlCompressedRedRgtc1Ext => CompressionFormat.Bc4, + GlInternalFormat.GlCompressedRedGreenRgtc2Ext => CompressionFormat.Bc5, + GlInternalFormat.GlCompressedRgbBptcUnsignedFloatArb => CompressionFormat.Bc6U, + GlInternalFormat.GlCompressedRgbBptcSignedFloatArb => CompressionFormat.Bc6S, + // TODO: Not sure what to do with SRGB input. + GlInternalFormat.GlCompressedRgbaBptcUnormArb or GlInternalFormat.GlCompressedSrgbAlphaBptcUnormArb => CompressionFormat.Bc7, + GlInternalFormat.GlCompressedRgbAtc => CompressionFormat.Atc, + GlInternalFormat.GlCompressedRgbaAtcExplicitAlpha => CompressionFormat.AtcExplicitAlpha, + GlInternalFormat.GlCompressedRgbaAtcInterpolatedAlpha => CompressionFormat.AtcInterpolatedAlpha, + _ => CompressionFormat.Unknown, + }; + } + + private CompressionFormat GetCompressionFormat(DdsFile file) + { + var format = file.header.ddsPixelFormat.IsDxt10Format ? + file.dx10Header.dxgiFormat : + file.header.ddsPixelFormat.DxgiFormat; + + switch (format) + { + case DxgiFormat.DxgiFormatR8Unorm: + return CompressionFormat.R; + + case DxgiFormat.DxgiFormatR8G8Unorm: + return CompressionFormat.Rg; + + // HINT: R8G8B8 has no DxgiFormat to convert from + + case DxgiFormat.DxgiFormatR8G8B8A8Unorm: + return CompressionFormat.Rgba; + + case DxgiFormat.DxgiFormatB8G8R8A8Unorm: + return CompressionFormat.Bgra; + + case DxgiFormat.DxgiFormatBc1Unorm: + case DxgiFormat.DxgiFormatBc1UnormSrgb: + case DxgiFormat.DxgiFormatBc1Typeless: + if (file.header.ddsPixelFormat.dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels)) + return CompressionFormat.Bc1WithAlpha; + + if (InputOptions.DdsBc1ExpectAlpha) + return CompressionFormat.Bc1WithAlpha; + + return CompressionFormat.Bc1; + + case DxgiFormat.DxgiFormatBc2Unorm: + case DxgiFormat.DxgiFormatBc2UnormSrgb: + case DxgiFormat.DxgiFormatBc2Typeless: + return CompressionFormat.Bc2; + + case DxgiFormat.DxgiFormatBc3Unorm: + case DxgiFormat.DxgiFormatBc3UnormSrgb: + case DxgiFormat.DxgiFormatBc3Typeless: + return CompressionFormat.Bc3; + + case DxgiFormat.DxgiFormatBc4Unorm: + case DxgiFormat.DxgiFormatBc4Snorm: + case DxgiFormat.DxgiFormatBc4Typeless: + return CompressionFormat.Bc4; + + case DxgiFormat.DxgiFormatBc5Unorm: + case DxgiFormat.DxgiFormatBc5Snorm: + case DxgiFormat.DxgiFormatBc5Typeless: + return CompressionFormat.Bc5; + + case DxgiFormat.DxgiFormatBc6HTypeless: + case DxgiFormat.DxgiFormatBc6HUf16: + return CompressionFormat.Bc6U; + + case DxgiFormat.DxgiFormatBc6HSf16: + return CompressionFormat.Bc6S; + + case DxgiFormat.DxgiFormatBc7Unorm: + case DxgiFormat.DxgiFormatBc7UnormSrgb: + case DxgiFormat.DxgiFormatBc7Typeless: + return CompressionFormat.Bc7; + + case DxgiFormat.DxgiFormatAtcExt: + return CompressionFormat.Atc; + + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + return CompressionFormat.AtcExplicitAlpha; + + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + return CompressionFormat.AtcInterpolatedAlpha; + + default: + return CompressionFormat.Unknown; + } + } + + private static int GetBufferSize(CompressionFormat format, int pixelWidth, int pixelHeight) + { + return format switch + { + CompressionFormat.R => pixelWidth * pixelHeight, + CompressionFormat.Rg => 2 * pixelWidth * pixelHeight, + CompressionFormat.Rgb => 3 * pixelWidth * pixelHeight, + CompressionFormat.Rgba or CompressionFormat.Bgra => 4 * pixelWidth * pixelHeight, + CompressionFormat.Bc1 or CompressionFormat.Bc1WithAlpha or CompressionFormat.Bc2 or CompressionFormat.Bc3 or CompressionFormat.Bc4 or CompressionFormat.Bc5 or CompressionFormat.Bc6S or CompressionFormat.Bc6U or CompressionFormat.Bc7 or CompressionFormat.Atc or CompressionFormat.AtcExplicitAlpha or CompressionFormat.AtcInterpolatedAlpha => GetBlockSize(format) * ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight), + CompressionFormat.Unknown => 0, + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), + }; + } + + #endregion + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderInputOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderInputOptions.cs new file mode 100644 index 0000000..ef4b46a --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderInputOptions.cs @@ -0,0 +1,16 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options +{ + /// + /// A class for the decoder input options. + /// + public class DecoderInputOptions + { + /// + /// The DDS file Format doesn't seem to have a standard for indicating whether a BC1 texture + /// includes 1bit of alpha. This option will assume that all Bc1 textures contain alpha. + /// If this option is false, but the dds header includes a DDPF_ALPHAPIXELS flag, alpha will be included. + /// Default is true. + /// + public bool DdsBc1ExpectAlpha { get; set; } = true; + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOptions.cs new file mode 100644 index 0000000..bf91cad --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOptions.cs @@ -0,0 +1,29 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options +{ + /// + /// General options for the decoder. + /// + public class DecoderOptions + { + /// + /// Whether the blocks should be decoded in parallel. This can be much faster than single-threaded decoding, + /// but is slow if multiple textures are being processed at the same time. + /// When a debugger is attached, the decoder defaults to single-threaded operation to ease debugging. + /// Default is false. + /// + /// Parallel execution will be ignored in RawDecoders, due to minimal performance gain. + public bool IsParallel { get; set; } + + /// + /// Determines how many tasks should be used for parallel processing. + /// + public int TaskCount { get; set; } = Environment.ProcessorCount; + + /// + /// The progress context for the operation. + /// + public IProgress Progress { get; set; } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOutputOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOutputOptions.cs new file mode 100644 index 0000000..1d2f109 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/Options/DecoderOutputOptions.cs @@ -0,0 +1,32 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder.Options +{ + /// + /// A class for the decoder output options. + /// + public class DecoderOutputOptions + { + /// + /// If true, when decoding from R8 raw format, + /// output pixels will have all colors set to the same value (greyscale). + /// Default is true. (Does not apply to BC4 format.) + /// + public bool RedAsLuminance { get; set; } = true; + + /// + /// The color channel to populate with the values of a BC4 block. + /// + public ColorComponent Bc4Component { get; set; } = ColorComponent.R; + + /// + /// The color channel to populate with the values of the first BC5 block. + /// + public ColorComponent Bc5Component1 { get; set; } = ColorComponent.R; + + /// + /// The color channel to populate with the values of the second BC5 block. + /// + public ColorComponent Bc5Component2 { get; set; } = ColorComponent.G; + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/RawDecoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/RawDecoder.cs new file mode 100644 index 0000000..4d5df8d --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Decoder/RawDecoder.cs @@ -0,0 +1,190 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder +{ + internal interface IRawDecoder + { + ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context); + } + + /// + /// A class to decode data to R components. + /// + public class RawRDecoder : IRawDecoder + { + private readonly bool redAsLuminance; + + /// + /// Create a new instance of . + /// + /// If the decoded component should be used as the red component or luminance. + public RawRDecoder(bool redAsLuminance) + { + this.redAsLuminance = redAsLuminance; + } + + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (redAsLuminance) + { + output[i].r = span[i]; + output[i].g = span[i]; + output[i].b = span[i]; + } + else + { + output[i].r = span[i]; + output[i].g = 0; + output[i].b = 0; + } + + output[i].a = 255; + } + + return output; + } + } + + /// + /// A class to decode data to RG components. + /// + public class RawRgDecoder : IRawDecoder + { + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 2]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].r = span[i * 2]; + output[i].g = span[i * 2 + 1]; + output[i].b = 0; + output[i].a = 255; + } + + return output; + } + } + + /// + /// A class to decode data to RGB components. + /// + public class RawRgbDecoder : IRawDecoder + { + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 3]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].r = span[i * 3]; + output[i].g = span[i * 3 + 1]; + output[i].b = span[i * 3 + 2]; + output[i].a = 255; + } + + return output; + } + } + + /// + /// A class to decode data to RGBA components. + /// + public class RawRgbaDecoder : IRawDecoder + { + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 4]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].r = span[i * 4]; + output[i].g = span[i * 4 + 1]; + output[i].b = span[i * 4 + 2]; + output[i].a = span[i * 4 + 3]; + } + + return output; + } + } + + /// + /// A class to decode data to BGRA components. + /// + public class RawBgraDecoder : IRawDecoder + { + /// + /// Decode the data to color components. + /// + /// The data to decode. + /// The context of the current operation. + /// The decoded color components. + public ColorRgba32[] Decode(ReadOnlyMemory data, OperationContext context) + { + var output = new ColorRgba32[data.Length / 4]; + + // HINT: Ignoring parallel execution since we wouldn't gain performance from it. + + var span = data.Span; + for (var i = 0; i < output.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + output[i].b = span[i * 4]; + output[i].g = span[i * 4 + 1]; + output[i].r = span[i * 4 + 2]; + output[i].a = span[i * 4 + 3]; + } + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/AtcBlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/AtcBlockEncoder.cs new file mode 100644 index 0000000..9c153a6 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/AtcBlockEncoder.cs @@ -0,0 +1,136 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal unsafe class AtcBlockEncoder : BaseBcBlockEncoder + { + private readonly Bc1BlockEncoder bc1BlockEncoder; + + public AtcBlockEncoder() + { + bc1BlockEncoder = new Bc1BlockEncoder(); + } + + public override AtcBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var atcBlock = new AtcBlock(); + + // EncodeBlock with BC1 first + var bc1Block = bc1BlockEncoder.EncodeBlock(block, quality); + + // Atc specific modifications to BC1 + // According to http://www.guildsoftware.com/papers/2012.Converting.DXTC.to.Atc.pdf + + // Change color0 from rgb565 to rgb555 with method 0 + atcBlock.color0 = new ColorRgb555(bc1Block.color0.R, bc1Block.color0.G, bc1Block.color0.B); + atcBlock.color1 = bc1Block.color1; + + // Remap color indices from BC1 to ATC + var remap = stackalloc byte[] { 0, 3, 1, 2 }; + for (var i = 0; i < 16; i++) + { + atcBlock[i] = remap[bc1Block[i]]; + } + + return atcBlock; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbAtc; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatAtcExt; + } + } + + internal class AtcExplicitAlphaBlockEncoder : BaseBcBlockEncoder + { + private readonly AtcBlockEncoder atcBlockEncoder; + + public AtcExplicitAlphaBlockEncoder() + { + atcBlockEncoder = new AtcBlockEncoder(); + } + + public override AtcExplicitAlphaBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var atcBlock = atcBlockEncoder.EncodeBlock(block, quality); + + // EncodeBlock alpha + var bc2AlphaBlock = new Bc2AlphaBlock(); + for (var i = 0; i < 16; i++) + { + bc2AlphaBlock.SetAlpha(i, block[i].a); + } + + return new AtcExplicitAlphaBlock + { + alphas = bc2AlphaBlock, + colors = atcBlock + }; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaAtcExplicitAlpha; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatAtcExplicitAlphaExt; + } + } + + internal class AtcInterpolatedAlphaBlockEncoder : BaseBcBlockEncoder + { + private readonly Bc4ComponentBlockEncoder bc4BlockEncoder; + private readonly AtcBlockEncoder atcBlockEncoder; + + public AtcInterpolatedAlphaBlockEncoder() + { + bc4BlockEncoder = new Bc4ComponentBlockEncoder(ColorComponent.A); + atcBlockEncoder = new AtcBlockEncoder(); + } + + public override AtcInterpolatedAlphaBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var bc4Block = bc4BlockEncoder.EncodeBlock(block, quality); + var atcBlock = atcBlockEncoder.EncodeBlock(block, quality); + + return new AtcInterpolatedAlphaBlock + { + alphas = bc4Block, + colors = atcBlock + }; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaAtcInterpolatedAlpha; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BaseBcBlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BaseBcBlockEncoder.cs new file mode 100644 index 0000000..4ad540d --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BaseBcBlockEncoder.cs @@ -0,0 +1,74 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal abstract class BaseBcBlockEncoder : IBcBlockEncoder where T : unmanaged where TBlock : unmanaged + { + private static readonly object lockObj = new object(); + + public byte[] Encode(TBlock[] blocks, int blockWidth, int blockHeight, CompressionQuality quality, OperationContext context) + { + var outputData = new byte[blockWidth * blockHeight * Unsafe.SizeOf()]; + + var currentBlocks = 0; + if (context.IsParallel) + { + var options = new ParallelOptions + { + CancellationToken = context.CancellationToken, + MaxDegreeOfParallelism = context.TaskCount + }; + Parallel.For(0, blocks.Length, options, i => + { + var outputBlocks = MemoryMarshal.Cast(outputData); + outputBlocks[i] = EncodeBlock(blocks[i], quality); + + if (context.Progress != null) + { + lock (lockObj) + { + context.Progress.Report(++currentBlocks); + } + } + }); + } + else + { + var outputBlocks = MemoryMarshal.Cast(outputData); + for (var i = 0; i < blocks.Length; i++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + outputBlocks[i] = EncodeBlock(blocks[i], quality); + + context.Progress?.Report(++currentBlocks); + } + } + + return outputData; + } + + public void EncodeBlock(TBlock block, CompressionQuality quality, Span output) + { + if (output.Length != Unsafe.SizeOf()) + { + throw new Exception("Cannot encode block! Output buffer is not the correct size."); + } + var encoded = EncodeBlock(block, quality); + MemoryMarshal.Cast(output)[0] = encoded; + } + + public abstract GlInternalFormat GetInternalFormat(); + public abstract GlFormat GetBaseInternalFormat(); + public abstract DxgiFormat GetDxgiFormat(); + public int GetBlockSize() + { + return Unsafe.SizeOf(); + } + + public abstract T EncodeBlock(TBlock block, CompressionQuality quality); + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc1BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc1BlockEncoder.cs new file mode 100644 index 0000000..29a2efe --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc1BlockEncoder.cs @@ -0,0 +1,473 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal class Bc1BlockEncoder : BaseBcBlockEncoder + { + public override Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc1BlockEncoderFast.EncodeBlock(block); + case CompressionQuality.Balanced: + return Bc1BlockEncoderBalanced.EncodeBlock(block); + case CompressionQuality.BestQuality: + return Bc1BlockEncoderSlow.EncodeBlock(block); + + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbS3TcDxt1Ext; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc1Unorm; + } + + #region Encoding private stuff + + private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) + { + var output = new Bc1Block(); + + var pixels = rawBlock.AsSpan; + + output.color0 = color0; + output.color1 = color1; + + var c0 = color0.ToColorRgb24(); + var c1 = color1.ToColorRgb24(); + + ReadOnlySpan colors = output.HasAlphaOrBlack ? + stackalloc ColorRgb24[] { + c0, + c1, + c0.InterpolateHalf(c1), + new ColorRgb24(0, 0, 0) + } : stackalloc ColorRgb24[] { + c0, + c1, + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) + }; + + error = 0; + for (var i = 0; i < 16; i++) + { + var color = pixels[i]; + output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e); + error += e; + } + + return output; + } + + + #endregion + + #region Encoders + + private static class Bc1BlockEncoderFast + { + + internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var output = new Bc1Block(); + + var pixels = rawBlock.AsSpan; + + RgbBoundingBox.Create565(pixels, out var min, out var max); + + var c0 = max; + var c1 = min; + + output = TryColors(rawBlock, c0, c1, out var error); + + return output; + } + } + + private static class Bc1BlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; + + internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (c0.data < c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + if (newC0.data < newC1.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + } + + if (bestError < ErrorThreshold) + { + break; + } + } + + return best; + } + } + + private static class Bc1BlockEncoderSlow + { + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.01f; + + internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (c0.data < c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + var lastChanged = 0; + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + if (newC0.data < newC1.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + lastChanged++; + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + lastChanged = 0; + } + + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { + break; + } + } + + return best; + } + } + + #endregion + } + + internal class Bc1AlphaBlockEncoder : BaseBcBlockEncoder + { + public override Bc1Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc1AlphaBlockEncoderFast.EncodeBlock(block); + case CompressionQuality.Balanced: + return Bc1AlphaBlockEncoderBalanced.EncodeBlock(block); + case CompressionQuality.BestQuality: + return Bc1AlphaBlockEncoderSlow.EncodeBlock(block); + + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaS3TcDxt1Ext; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc1Unorm; + } + + #region Encoding private stuff + + private static Bc1Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) + { + var output = new Bc1Block(); + + var pixels = rawBlock.AsSpan; + + output.color0 = color0; + output.color1 = color1; + + var c0 = color0.ToColorRgb24(); + var c1 = color1.ToColorRgb24(); + + var hasAlpha = output.HasAlphaOrBlack; + + ReadOnlySpan colors = hasAlpha ? + stackalloc ColorRgb24[] { + c0, + c1, + c0.InterpolateHalf(c1), + new ColorRgb24(0, 0, 0) + } : stackalloc ColorRgb24[] { + c0, + c1, + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) + }; + + error = 0; + for (var i = 0; i < 16; i++) + { + var color = pixels[i]; + output[i] = ColorChooser.ChooseClosestColor4AlphaCutoff(colors, color, rWeight, gWeight, bWeight, + 128, hasAlpha, out var e); + error += e; + } + + return output; + } + + #endregion + + #region Encoders + + private static class Bc1AlphaBlockEncoderFast + { + + internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var output = new Bc1Block(); + + var pixels = rawBlock.AsSpan; + + var hasAlpha = rawBlock.HasTransparentPixels(); + + RgbBoundingBox.Create565AlphaCutoff(pixels, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (hasAlpha && c0.data > c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + output = TryColors(rawBlock, c0, c1, out var error); + + return output; + } + } + + private static class Bc1AlphaBlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; + + + internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + var hasAlpha = rawBlock.HasTransparentPixels(); + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (!hasAlpha && c0.data < c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + else if (hasAlpha && c1.data < c0.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + if (!hasAlpha && newC0.data < newC1.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + else if (hasAlpha && newC1.data < newC0.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + } + + if (bestError < ErrorThreshold) + { + break; + } + } + + return best; + } + } + + private static class Bc1AlphaBlockEncoderSlow + { + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.05f; + + internal static Bc1Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + var hasAlpha = rawBlock.HasTransparentPixels(); + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (!hasAlpha && c0.data < c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + else if (hasAlpha && c1.data < c0.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + var lastChanged = 0; + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + if (!hasAlpha && newC0.data < newC1.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + else if (hasAlpha && newC1.data < newC0.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + lastChanged++; + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + lastChanged = 0; + } + + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { + break; + } + } + + return best; + } + } + + #endregion + + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc2BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc2BlockEncoder.cs new file mode 100644 index 0000000..a38e750 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc2BlockEncoder.cs @@ -0,0 +1,200 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal class Bc2BlockEncoder : BaseBcBlockEncoder + { + public override Bc2Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc2BlockEncoderFast.EncodeBlock(block); + case CompressionQuality.Balanced: + return Bc2BlockEncoderBalanced.EncodeBlock(block); + case CompressionQuality.BestQuality: + return Bc2BlockEncoderSlow.EncodeBlock(block); + + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaS3TcDxt3Ext; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc2Unorm; + } + + #region Encoding private stuff + + private static Bc2Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) + { + var output = new Bc2Block(); + + var pixels = rawBlock.AsSpan; + + output.color0 = color0; + output.color1 = color1; + + var c0 = color0.ToColorRgb24(); + var c1 = color1.ToColorRgb24(); + + ReadOnlySpan colors = stackalloc ColorRgb24[] { + c0, + c1, + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) + }; + + error = 0; + for (var i = 0; i < 16; i++) + { + var color = pixels[i]; + output.SetAlpha(i, color.a); + output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e); + error += e; + } + + return output; + } + + + #endregion + + #region Encoders + + private static class Bc2BlockEncoderFast + { + + internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var principalAxis); + PcaVectors.GetMinMaxColor565(pixels, mean, principalAxis, out var min, out var max); + + var c0 = max; + var c1 = min; + + var output = TryColors(rawBlock, c0, c1, out var _); + + return output; + } + } + + private static class Bc2BlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; + + internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + } + + if (bestError < ErrorThreshold) + { + break; + } + } + + return best; + } + } + + private static class Bc2BlockEncoderSlow + { + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.01f; + + + internal static Bc2Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (c0.data < c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + var lastChanged = 0; + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + if (newC0.data < newC1.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + lastChanged++; + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + lastChanged = 0; + } + + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { + break; + } + } + + return best; + } + } + + #endregion + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc3BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc3BlockEncoder.cs new file mode 100644 index 0000000..8f056b4 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc3BlockEncoder.cs @@ -0,0 +1,207 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal class Bc3BlockEncoder : BaseBcBlockEncoder + { + private static readonly Bc4ComponentBlockEncoder bc4BlockEncoder = new Bc4ComponentBlockEncoder(ColorComponent.A); + + public override Bc3Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc3BlockEncoderFast.EncodeBlock(block); + case CompressionQuality.Balanced: + return Bc3BlockEncoderBalanced.EncodeBlock(block); + case CompressionQuality.BestQuality: + return Bc3BlockEncoderSlow.EncodeBlock(block); + + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaS3TcDxt5Ext; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc3Unorm; + } + + #region Encoding private stuff + + private static Bc3Block TryColors(RawBlock4X4Rgba32 rawBlock, ColorRgb565 color0, ColorRgb565 color1, out float error, float rWeight = 0.3f, float gWeight = 0.6f, float bWeight = 0.1f) + { + var output = new Bc3Block(); + + var pixels = rawBlock.AsSpan; + + output.color0 = color0; + output.color1 = color1; + + var c0 = color0.ToColorRgb24(); + var c1 = color1.ToColorRgb24(); + + ReadOnlySpan colors = stackalloc ColorRgb24[] { + c0, + c1, + c0.InterpolateThird(c1, 1), + c0.InterpolateThird(c1, 2) + }; + + error = 0; + for (var i = 0; i < 16; i++) + { + var color = pixels[i]; + output[i] = ColorChooser.ChooseClosestColor4(colors, color, rWeight, gWeight, bWeight, out var e); + error += e; + } + + return output; + } + + #endregion + + #region Encoders + + private static class Bc3BlockEncoderFast + { + internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var principalAxis); + PcaVectors.GetMinMaxColor565(pixels, mean, principalAxis, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (c0.data <= c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var output = TryColors(rawBlock, c0, c1, out _); + output.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.Fast); + + return output; + } + } + + private static class Bc3BlockEncoderBalanced + { + private const int MaxTries = 24 * 2; + private const float ErrorThreshold = 0.05f; + + internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + } + + if (bestError < ErrorThreshold) + { + break; + } + } + best.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.Balanced); + return best; + } + } + + private static class Bc3BlockEncoderSlow + { + private const int MaxTries = 9999; + private const float ErrorThreshold = 0.01f; + + + internal static Bc3Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var pixels = rawBlock.AsSpan; + + PcaVectors.Create(pixels, out var mean, out var pa); + PcaVectors.GetMinMaxColor565(pixels, mean, pa, out var min, out var max); + + var c0 = max; + var c1 = min; + + if (c0.data < c1.data) + { + var c = c0; + c0 = c1; + c1 = c; + } + + var best = TryColors(rawBlock, c0, c1, out var bestError); + + var lastChanged = 0; + + for (var i = 0; i < MaxTries; i++) + { + var (newC0, newC1) = ColorVariationGenerator.Variate565(c0, c1, i); + + if (newC0.data < newC1.data) + { + var c = newC0; + newC0 = newC1; + newC1 = c; + } + + var block = TryColors(rawBlock, newC0, newC1, out var error); + + lastChanged++; + + if (error < bestError) + { + best = block; + bestError = error; + c0 = newC0; + c1 = newC1; + lastChanged = 0; + } + + if (bestError < ErrorThreshold || lastChanged > ColorVariationGenerator.VarPatternCount) + { + break; + } + } + + best.alphaBlock = bc4BlockEncoder.EncodeBlock(rawBlock, CompressionQuality.BestQuality); + return best; + } + } + #endregion + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc4BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc4BlockEncoder.cs new file mode 100644 index 0000000..4bf766b --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc4BlockEncoder.cs @@ -0,0 +1,268 @@ +using System.Diagnostics; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal class Bc4BlockEncoder : BaseBcBlockEncoder + { + private readonly Bc4ComponentBlockEncoder bc4Encoder; + + public Bc4BlockEncoder(ColorComponent component) + { + bc4Encoder = new Bc4ComponentBlockEncoder(component); + } + + public override Bc4Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var output = new Bc4Block + { + componentBlock = bc4Encoder.EncodeBlock(block, quality) + }; + + return output; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRedRgtc1Ext; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRed; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc4Unorm; + } + } + + internal class Bc4ComponentBlockEncoder + { + private readonly ColorComponent component; + + public Bc4ComponentBlockEncoder(ColorComponent component) + { + this.component = component; + } + + public Bc4ComponentBlock EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + var output = new Bc4ComponentBlock(); + + var pixels = block.AsSpan; + var colors = new byte[pixels.Length]; + + for (var i = 0; i < pixels.Length; i++) + colors[i] = ComponentHelper.ColorToComponent(pixels[i], component); + + switch (quality) + { + case CompressionQuality.Fast: + return FindComponentValues(output, colors, 3); + case CompressionQuality.Balanced: + return FindComponentValues(output, colors, 4); + case CompressionQuality.BestQuality: + return FindComponentValues(output, colors, 8); + + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + #region Encoding private stuff + + private static Bc4ComponentBlock FindComponentValues(Bc4ComponentBlock colorBlock, byte[] pixels, int variations) + { + + //Find min and max alpha + byte min = 255; + byte max = 0; + var hasExtremeValues = false; + for (var i = 0; i < pixels.Length; i++) + { + if (pixels[i] < 255 && pixels[i] > 0) + { + if (pixels[i] < min) min = pixels[i]; + if (pixels[i] > max) max = pixels[i]; + } + else + { + hasExtremeValues = true; + } + } + + + int SelectIndices(ref Bc4ComponentBlock block) + { + var cumulativeError = 0; + var c0 = block.Endpoint0; + var c1 = block.Endpoint1; + var colors = c0 > c1 ? stackalloc byte[] { + c0, + c1, + c0.InterpolateSeventh(c1, 1), + c0.InterpolateSeventh(c1, 2), + c0.InterpolateSeventh(c1, 3), + c0.InterpolateSeventh(c1, 4), + c0.InterpolateSeventh(c1, 5), + c0.InterpolateSeventh(c1, 6) + } : stackalloc byte[] { + c0, + c1, + c0.InterpolateFifth(c1, 1), + c0.InterpolateFifth(c1, 2), + c0.InterpolateFifth(c1, 3), + c0.InterpolateFifth(c1, 4), + 0, + 255 + }; + for (var i = 0; i < pixels.Length; i++) + { + byte bestIndex = 0; + var bestError = Math.Abs(pixels[i] - colors[0]); + for (byte j = 1; j < colors.Length; j++) + { + var error = Math.Abs(pixels[i] - colors[j]); + if (error < bestError) + { + bestIndex = j; + bestError = error; + } + + if (bestError == 0) break; + } + + block.SetComponentIndex(i, bestIndex); + cumulativeError += bestError * bestError; + } + + return cumulativeError; + } + + //everything is either fully black or fully red + if (hasExtremeValues && min == 255 && max == 0) + { + colorBlock.Endpoint0 = 0; + colorBlock.Endpoint1 = 255; + var error = SelectIndices(ref colorBlock); + Debug.Assert(0 == error); + return colorBlock; + } + + var best = colorBlock; + best.Endpoint0 = max; + best.Endpoint1 = min; + var bestError = SelectIndices(ref best); + if (bestError == 0) + { + return best; + } + + for (var i = (byte)variations; i > 0; i--) + { + { + var c0 = ByteHelper.ClampToByte(max - i); + var c1 = ByteHelper.ClampToByte(min + i); + var block = colorBlock; + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { + best = block; + bestError = error; + max = c0; + min = c1; + } + } + { + var c0 = ByteHelper.ClampToByte(max + i); + var c1 = ByteHelper.ClampToByte(min - i); + var block = colorBlock; + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { + best = block; + bestError = error; + max = c0; + min = c1; + } + } + { + var c0 = ByteHelper.ClampToByte(max); + var c1 = ByteHelper.ClampToByte(min - i); + var block = colorBlock; + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { + best = block; + bestError = error; + max = c0; + min = c1; + } + } + { + var c0 = ByteHelper.ClampToByte(max + i); + var c1 = ByteHelper.ClampToByte(min); + var block = colorBlock; + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { + best = block; + bestError = error; + max = c0; + min = c1; + } + } + { + var c0 = ByteHelper.ClampToByte(max); + var c1 = ByteHelper.ClampToByte(min + i); + var block = colorBlock; + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { + best = block; + bestError = error; + max = c0; + min = c1; + } + } + { + var c0 = ByteHelper.ClampToByte(max - i); + var c1 = ByteHelper.ClampToByte(min); + var block = colorBlock; + block.Endpoint0 = hasExtremeValues ? c1 : c0; + block.Endpoint1 = hasExtremeValues ? c0 : c1; + var error = SelectIndices(ref block); + if (error < bestError) + { + best = block; + bestError = error; + max = c0; + min = c1; + } + } + + if (bestError < 5) + { + break; + } + } + + return best; + } + + #endregion + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc5BlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc5BlockEncoder.cs new file mode 100644 index 0000000..5db2aed --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bc5BlockEncoder.cs @@ -0,0 +1,41 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal class Bc5BlockEncoder : BaseBcBlockEncoder + { + private readonly Bc4ComponentBlockEncoder redBlockEncoder; + private readonly Bc4ComponentBlockEncoder greenBlockEncoder; + + public Bc5BlockEncoder(ColorComponent component1, ColorComponent component2) + { + redBlockEncoder = new Bc4ComponentBlockEncoder(component1); + greenBlockEncoder = new Bc4ComponentBlockEncoder(component2); + } + + public override Bc5Block EncodeBlock(RawBlock4X4Rgba32 block, CompressionQuality quality) + { + return new Bc5Block + { + redBlock = redBlockEncoder.EncodeBlock(block, quality), + greenBlock = greenBlockEncoder.EncodeBlock(block, quality) + }; + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRedGreenRgtc2Ext; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRg; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc5Unorm; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BcEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BcEncoder.cs new file mode 100644 index 0000000..85a33b8 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/BcEncoder.cs @@ -0,0 +1,2185 @@ +using CommunityToolkit.HighPerformance; +using System.Diagnostics; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Options; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + /// + /// The pixel format determining the rgba layout of input data in . + /// + public enum PixelFormat + { + /// + /// 8 bits per channel RGBA. + /// + Rgba32, + + /// + /// 8 bits per channel BGRA. + /// + Bgra32, + + /// + /// 8 bits per channel ARGB. + /// + Argb32, + + /// + /// 8 bits per channel RGB. + /// + Rgb24, + + /// + /// 8 bits per channel BGR. + /// + Bgr24 + } + + /// + /// Handles all encoding of images into compressed or uncompressed formats. For decoding, + /// + public class BcEncoder + { + /// + /// The input options of the encoder. + /// + public EncoderInputOptions InputOptions { get; } = new EncoderInputOptions(); + + /// + /// The output options of the encoder. + /// + public EncoderOutputOptions OutputOptions { get; } = new EncoderOutputOptions(); + + /// + /// The encoder options. + /// + public EncoderOptions Options { get; } = new EncoderOptions(); + + /// + /// Creates a new instance of . + /// + /// The block compression Format to encode an image with. + public BcEncoder(CompressionFormat format = CompressionFormat.Bc1) + { + OutputOptions.Format = format; + } + + #region LDR + #region Async Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + public Task EncodeToStreamAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => + { + EncodeToStreamInternal(ByteToColorMemory(input.Span, width, height, format), outputStream, token); + }, token); + } + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + public Task EncodeToStreamAsync(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => + { + EncodeToStreamInternal(input, outputStream, default); + }, token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Ktx file containing the encoded image. + public Task EncodeToKtxAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default) + { + return Task.Run(() => EncodeToKtxInternal(ByteToColorMemory(input.Span, width, height, format), token), token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Ktx file containing the encoded image. + public Task EncodeToKtxAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToKtxInternal(input, token), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Dds file containing the encoded image. + public Task EncodeToDdsAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default) + { + return Task.Run(() => EncodeToDdsInternal(ByteToColorMemory(input.Span, width, height, format), token), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Dds file containing the encoded image. + public Task EncodeToDdsAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToDdsInternal(input, token), token); + } + + /// + /// Encodes all mipmap levels into a list of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A list of raw encoded mipmap input. + public Task EncodeToRawBytesAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(ByteToColorMemory(input.Span, width, height, format), token), token); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A list of raw encoded mipmap input. + /// To get the width and height of the encoded mip levels, see . + public Task EncodeToRawBytesAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(input, token), token); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the given data is in. + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default, if the operation is not asynchronous. + /// The raw encoded input. + /// To get the width and height of the encoded mip level, see . + public Task EncodeToRawBytesAsync(ReadOnlyMemory input, int width, int height, PixelFormat format, int mipLevel, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(ByteToColorMemory(input.Span, width, height, format), mipLevel, out _, out _, token), token); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The raw encoded input. + /// To get the width and height of the encoded mip level, see . + public Task EncodeToRawBytesAsync(ReadOnlyMemory2D input, int mipLevel, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternal(input, mipLevel, out _, out _, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a stream asynchronously either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A . + public Task EncodeCubeMapToStreamAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToStreamInternal(right, left, top, down, back, front, outputStream, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToKtxAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToKtxInternal(right, left, top, down, back, front, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToDdsAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToDdsInternal(right, left, top, down, back, front, token), token); + } + + #endregion + + #region Sync Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The stream to write the encoded image to. + public void EncodeToStream(ReadOnlySpan input, int width, int height, PixelFormat format, Stream outputStream) + { + EncodeToStream(ByteToColorMemory(input, width, height, format), outputStream); + } + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + public void EncodeToStream(ReadOnlyMemory2D input, Stream outputStream) + { + EncodeToStreamInternal(input, outputStream, default); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The containing the encoded image. + public KtxFile EncodeToKtx(ReadOnlySpan input, int width, int height, PixelFormat format) + { + return EncodeToKtx(ByteToColorMemory(input, width, height, format)); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public KtxFile EncodeToKtx(ReadOnlyMemory2D input) + { + return EncodeToKtxInternal(input, default); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The containing the encoded image. + public DdsFile EncodeToDds(ReadOnlySpan input, int width, int height, PixelFormat format) + { + return EncodeToDds(ByteToColorMemory(input, width, height, format)); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public DdsFile EncodeToDds(ReadOnlyMemory2D input) + { + return EncodeToDdsInternal(input, default); + } + + /// + /// Encodes all mipmap levels into an array of byte buffers. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// An array of byte buffers containing all mipmap levels. + /// To get the width and height of the encoded mip levels, see . + public byte[][] EncodeToRawBytes(ReadOnlySpan input, int width, int height, PixelFormat format) + { + return EncodeToRawBytes(ByteToColorMemory(input, width, height, format)); + } + + /// + /// Encodes all mipmap levels into a list of byte buffers. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// An array of byte buffers containing all mipmap levels. + /// To get the width and height of the encoded mip levels, see . + public byte[][] EncodeToRawBytes(ReadOnlyMemory2D input) + { + return EncodeToRawInternal(input, default); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The width of the image. + /// The height of the image. + /// The pixel format the input data is in. + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// A byte buffer containing the encoded data of the requested mip-level. + public byte[] EncodeToRawBytes(ReadOnlySpan input, int width, int height, PixelFormat format, int mipLevel, out int mipWidth, out int mipHeight) + { + return EncodeToRawInternal(ByteToColorMemory(input, width, height, format), mipLevel, out mipWidth, out mipHeight, default); + } + + /// + /// Encodes a single mip level of the input image to a byte buffer. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// A byte buffer containing the encoded data of the requested mip-level. + public byte[] EncodeToRawBytes(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight) + { + return EncodeToRawInternal(input, mipLevel, out mipWidth, out mipHeight, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a stream either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The width of the faces. + /// The height of the faces. + /// The pixel format the input data is in. + /// The stream to write the encoded image to. + public void EncodeCubeMapToStream(ReadOnlySpan right, ReadOnlySpan left, + ReadOnlySpan top, ReadOnlySpan down, + ReadOnlySpan back, ReadOnlySpan front, + int width, int height, PixelFormat format, Stream outputStream) + { + EncodeCubeMapToStreamInternal( + ByteToColorMemory(right, width, height, format), + ByteToColorMemory(left, width, height, format), + ByteToColorMemory(top, width, height, format), + ByteToColorMemory(down, width, height, format), + ByteToColorMemory(back, width, height, format), + ByteToColorMemory(front, width, height, format), + outputStream, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a stream either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + public void EncodeCubeMapToStream(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream) + { + EncodeCubeMapToStreamInternal(right, left, top, down, back, front, outputStream, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The width of the faces. + /// The height of the faces. + /// The pixel format the input data is in. + /// The encoded image as a . + public KtxFile EncodeCubeMapToKtx(ReadOnlySpan right, ReadOnlySpan left, + ReadOnlySpan top, ReadOnlySpan down, + ReadOnlySpan back, ReadOnlySpan front, + int width, int height, PixelFormat format) + { + return EncodeCubeMapToKtxInternal( + ByteToColorMemory(right, width, height, format), + ByteToColorMemory(left, width, height, format), + ByteToColorMemory(top, width, height, format), + ByteToColorMemory(down, width, height, format), + ByteToColorMemory(back, width, height, format), + ByteToColorMemory(front, width, height, format), default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public KtxFile EncodeCubeMapToKtx(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToKtxInternal(right, left, top, down, back, front, default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The width of the faces. + /// The height of the faces. + /// The pixel format the input data is in. + /// The encoded image as a . + public DdsFile EncodeCubeMapToDds(ReadOnlySpan right, ReadOnlySpan left, + ReadOnlySpan top, ReadOnlySpan down, + ReadOnlySpan back, ReadOnlySpan front, + int width, int height, PixelFormat format) + { + return EncodeCubeMapToDdsInternal( + ByteToColorMemory(right, width, height, format), + ByteToColorMemory(left, width, height, format), + ByteToColorMemory(top, width, height, format), + ByteToColorMemory(down, width, height, format), + ByteToColorMemory(back, width, height, format), + ByteToColorMemory(front, width, height, format), default); + } + + /// + /// Encodes all mipMaps of a cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public DdsFile EncodeCubeMapToDds(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToDdsInternal(right, left, top, down, back, front, default); + } + + /// + /// Encodes a single 4x4 block to raw encoded bytes. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlock(ReadOnlySpan inputBlock) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternal(inputBlock.AsSpan2D(4, 4)); + } + + /// + /// Encodes a single 4x4 block to raw encoded bytes. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlock(ReadOnlySpan2D inputBlock) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternal(inputBlock); + } + + /// + /// Encodes a single 4x4 block and writes the encoded block to a stream. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlock(ReadOnlySpan inputBlock, Stream outputStream) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternal(inputBlock.AsSpan2D(4, 4), outputStream); + } + + /// + /// Encodes a single 4x4 block and writes the encoded block to a stream. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlock(ReadOnlySpan2D inputBlock, Stream outputStream) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternal(inputBlock, outputStream); + } + + /// + /// Gets the block size of the currently selected compression format in bytes. + /// + /// The size of a single 4x4 block in bytes + public int GetBlockSize() + { + var compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + var hdrEncoder = GetFloatBlockEncoder(OutputOptions.Format); + + return hdrEncoder switch + { + null => throw new NotSupportedException($"This format is either not supported or does not use block compression: {OutputOptions.Format}"), + _ => hdrEncoder.GetBlockSize() + }; + } + return compressedEncoder.GetBlockSize(); + } + + /// + /// Gets the number of total blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The total number of blocks. + public static int GetBlockCount(int pixelWidth, int pixelHeight) + { + return ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight); + } + + /// + /// Gets the number of blocks in an image with the given pixel width and height. + /// + /// The pixel width of the image + /// The pixel height of the image + /// The amount of blocks in the x-axis + /// The amount of blocks in the y-axis + public static void GetBlockCount(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight) + { + ImageToBlocks.CalculateNumOfBlocks(pixelWidth, pixelHeight, out blocksWidth, out blocksHeight); + } + + #endregion + #endregion + + #region HDR + + #region Async Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream asynchronously. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + public Task EncodeToStreamHdrAsync(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => + { + EncodeToStreamInternalHdr(input, outputStream, default); + }, token); + } + + /// + /// Encodes all mipmap levels into a Ktx file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Ktx file containing the encoded image. + public Task EncodeToKtxHdrAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToKtxInternalHdr(input, token), token); + } + + /// + /// Encodes all mipmap levels into a Dds file asynchronously. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The Dds file containing the encoded image. + public Task EncodeToDdsHdrAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToDdsInternalHdr(input, token), token); + } + + /// + /// Encodes all mipmap levels of an HDR image into an array of byte buffers asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A list of raw encoded mipmap input. + /// To get the width and height of the encoded mip levels, see . + public Task EncodeToRawBytesHdrAsync(ReadOnlyMemory2D input, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternalHdr(input, token), token); + } + + /// + /// Encodes a single mip level of the input HDR image to a byte buffer asynchronously. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// The raw encoded input. + /// To get the width and height of the encoded mip level, see . + public Task EncodeToRawBytesHdrAsync(ReadOnlyMemory2D input, int mipLevel, CancellationToken token = default) + { + return Task.Run(() => EncodeToRawInternalHdr(input, mipLevel, out _, out _, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap HDR image to a stream asynchronously either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A . + public Task EncodeCubeMapToStreamHdrAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToStreamInternalHdr(right, left, top, down, back, front, outputStream, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap HDR image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToKtxHdrAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToKtxInternalHdr(right, left, top, down, back, front, token), token); + } + + /// + /// Encodes all mipMaps of a cubeMap HDR image to a asynchronously. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The cancellation token for this operation. Can be default if cancellation is not needed. + /// A of type . + public Task EncodeCubeMapToDdsHdrAsync(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token = default) + { + return Task.Run(() => EncodeCubeMapToDdsInternalHdr(right, left, top, down, back, front, token), token); + } + + #endregion + + #region Sync Api + + /// + /// Encodes all mipmap levels into a ktx or a dds file and writes it to the output stream. + /// + /// The input to encode represented by a . + /// The stream to write the encoded image to. + public void EncodeToStreamHdr(ReadOnlyMemory2D input, Stream outputStream) + { + EncodeToStreamInternalHdr(input, outputStream, default); + } + + /// + /// Encodes all mipmap levels into a Ktx file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public KtxFile EncodeToKtxHdr(ReadOnlyMemory2D input) + { + return EncodeToKtxInternalHdr(input, default); + } + + /// + /// Encodes all mipmap levels into a Dds file. + /// + /// The input to encode represented by a . + /// The containing the encoded image. + public DdsFile EncodeToDdsHdr(ReadOnlyMemory2D input) + { + return EncodeToDdsInternalHdr(input, default); + } + + /// + /// Encodes all mipmap levels of a HDR image into a list of byte buffers. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// An array of byte buffers containing all mipmap levels. + /// To get the width and height of the encoded mip levels, see . + public byte[][] EncodeToRawBytesHdr(ReadOnlyMemory2D input) + { + return EncodeToRawInternalHdr(input, default); + } + + /// + /// Encodes a single mip level of the HDR input image to a byte buffer. This data does not contain any file headers, just the raw encoded pixel data. + /// + /// The input to encode represented by a . + /// The mipmap to encode. + /// The width of the mipmap. + /// The height of the mipmap. + /// A byte buffer containing the encoded data of the requested mip-level. + public byte[] EncodeToRawBytesHdr(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight) + { + return EncodeToRawInternalHdr(input, mipLevel, out mipWidth, out mipHeight, default); + } + + /// + /// Encodes all mipMaps of a HDR cubeMap image to a stream either in ktx or dds format. + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The stream to write the encoded image to. + public void EncodeCubeMapToStreamHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream) + { + EncodeCubeMapToStreamInternalHdr(right, left, top, down, back, front, outputStream, default); + } + + /// + /// Encodes all mipMaps of a HDR cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public KtxFile EncodeCubeMapToKtxHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToKtxInternalHdr(right, left, top, down, back, front, default); + } + + /// + /// Encodes all mipMaps of a HDR cubeMap image to a . + /// The format can be set in . + /// Order of faces is +X, -X, +Y, -Y, +Z, -Z. Back maps to positive Z and front to negative Z. + /// + /// The positive X-axis face of the cubeMap + /// The negative X-axis face of the cubeMap + /// The positive Y-axis face of the cubeMap + /// The negative Y-axis face of the cubeMap + /// The positive Z-axis face of the cubeMap + /// The negative Z-axis face of the cubeMap + /// The encoded image as a . + public DdsFile EncodeCubeMapToDdsHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, + ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front) + { + return EncodeCubeMapToDdsInternalHdr(right, left, top, down, back, front, default); + } + + /// + /// Encodes a single 4x4 HDR block to raw encoded bytes. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlockHdr(ReadOnlySpan inputBlock) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternalHdr(inputBlock.AsSpan2D(4, 4)); + } + + /// + /// Encodes a single 4x4 HDR block to raw encoded bytes. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Raw encoded data + public byte[] EncodeBlockHdr(ReadOnlySpan2D inputBlock) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + return EncodeBlockInternalHdr(inputBlock); + } + + /// + /// Encodes a single 4x4 HDR block and writes the encoded block to a stream. Input Span length must be exactly 16. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlockHdr(ReadOnlySpan inputBlock, Stream outputStream) + { + if (inputBlock.Length != 16) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternalHdr(inputBlock.AsSpan2D(4, 4), outputStream); + } + + /// + /// Encodes a single 4x4 HDR block and writes the encoded block to a stream. Input Span width and height must be exactly 4. + /// + /// Input 4x4 color block + /// Output stream where the encoded block will be written to. + public void EncodeBlockHdr(ReadOnlySpan2D inputBlock, Stream outputStream) + { + if (inputBlock.Width != 4 || inputBlock.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + EncodeBlockInternalHdr(inputBlock, outputStream); + } + + #endregion + + #endregion + #region MipMap operations + + /// + /// Calculates the number of mipmap levels that will be generated for the given input image. + /// + /// The width of the input image in pixels + /// The height of the input image in pixels + /// The number of mipmap levels that will be generated for the input image. + public int CalculateNumberOfMipLevels(int imagePixelWidth, int imagePixelHeight) + { + return MipMapper.CalculateMipChainLength(imagePixelWidth, imagePixelHeight, + OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1); + } + + /// + /// Calculates the size of a given mipmap level. + /// + /// The width of the input image in pixels + /// The height of the input image in pixels + /// The mipLevel to calculate (0 is original image) + /// The mipmap width calculated + /// The mipmap height calculated + public static void CalculateMipMapSize(int imagePixelWidth, int imagePixelHeight, int mipLevel, out int mipWidth, out int mipHeight) + { + MipMapper.CalculateMipLevelSize(imagePixelWidth, imagePixelHeight, mipLevel, out mipWidth, out mipHeight); + } + + #endregion + + #region Private + + #region HDR + + private void EncodeToStreamInternalHdr(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token) + { + switch (OutputOptions.FileFormat) + { + case OutputFileFormat.Dds: + var dds = EncodeToDdsInternalHdr(input, token); + dds.Write(outputStream); + break; + + case OutputFileFormat.Ktx: + var ktx = EncodeToKtxInternalHdr(input, token); + ktx.Write(outputStream); + break; + } + } + + private KtxFile EncodeToKtxInternalHdr(ReadOnlyMemory2D input, CancellationToken token) + { + KtxFile output; + Bc6Encoder compressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoders + if (!OutputOptions.Format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not supported for hdr images: {OutputOptions.Format}"); + } + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + output = new KtxFile( + KtxHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetInternalFormat(), + compressedEncoder.GetBaseInternalFormat())); + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, + out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, + context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + + + output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height, 1)); + output.MipMaps[mip].Faces[0] = new KtxMipFace(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + + output.header.NumberOfFaces = 1; + output.header.NumberOfMipmapLevels = (uint)numMipMaps; + + return output; + } + + private DdsFile EncodeToDdsInternalHdr(ReadOnlyMemory2D input, CancellationToken token) + { + DdsFile output; + Bc6Encoder compressedEncoder; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + if (!OutputOptions.Format.IsHdrFormat()) + { + throw new NotSupportedException($"This Format is not supported for hdr images: {OutputOptions.Format}"); + } + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); + output = new DdsFile(ddsHeader, dxt10Header); + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + + + if (mip == 0) + { + output.Faces.Add(new DdsFace((uint)input.Width, (uint)input.Height, + (uint)encoded.Length, numMipMaps)); + } + + output.Faces[0].MipMaps[mip] = new DdsMipMap(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + + + output.header.dwMipMapCount = (uint)numMipMaps; + if (numMipMaps > 1) + { + output.header.dwCaps |= HeaderCaps.DdscapsComplex | HeaderCaps.DdscapsMipmap; + } + + return output; + } + + private byte[][] EncodeToRawInternalHdr(ReadOnlyMemory2D input, CancellationToken token) + { + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + var output = new byte[numMipMaps][]; + Bc6Encoder compressedEncoder = null; + + // Setup encoder + + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode all mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + + output[mip] = encoded; + } + + return output; + } + + private byte[] EncodeToRawInternalHdr(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight, CancellationToken token) + { + mipLevel = Math.Max(0, mipLevel); + + Bc6Encoder compressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + // Dispose all mipmap levels + if (mipLevel > numMipMaps - 1) + { + throw new ArgumentException($"{nameof(mipLevel)} cannot be more than number of mipmaps."); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap level + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + mipWidth = mipChain[mipLevel].Width; + mipHeight = mipChain[mipLevel].Height; + + return encoded; + } + + private void EncodeCubeMapToStreamInternalHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token) + { + switch (OutputOptions.FileFormat) + { + case OutputFileFormat.Ktx: + var ktx = EncodeCubeMapToKtxInternalHdr(right, left, top, down, back, front, token); + ktx.Write(outputStream); + break; + + case OutputFileFormat.Dds: + var dds = EncodeCubeMapToDdsInternalHdr(right, left, top, down, back, front, token); + dds.Write(outputStream); + break; + } + } + + private KtxFile EncodeCubeMapToKtxInternalHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) + { + KtxFile output; + Bc6Encoder compressedEncoder; + + var faces = new[] { right, left, top, down, back, front }; + + var width = right.Width; + var height = right.Height; + + // Setup encoder + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + output = new KtxFile( + KtxHeader.InitializeCompressed(width, height, + compressedEncoder.GetInternalFormat(), + compressedEncoder.GetBaseInternalFormat())); + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipLength = MipMapper.CalculateMipChainLength(width, height, numMipMaps); + for (uint i = 0; i < mipLength; i++) + { + output.MipMaps.Add(new KtxMipmap(0, 0, 0, (uint)faces.Length)); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight); + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); + + // Encode all mipmap levels per face + for (var mipLevel = 0; mipLevel < numMipMaps; mipLevel++) + { + byte[] encoded; + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + + if (face == 0) + { + output.MipMaps[mipLevel] = new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height, (uint)faces.Length); + } + + output.MipMaps[mipLevel].Faces[face] = new KtxMipFace(encoded, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height); + } + } + + output.header.NumberOfFaces = (uint)faces.Length; + output.header.NumberOfMipmapLevels = (uint)mipLength; + + return output; + } + + private DdsFile EncodeCubeMapToDdsInternalHdr(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) + { + DdsFile output; + Bc6Encoder compressedEncoder; + + var faces = new[] { right, left, top, down, back, front }; + + var width = right.Width; + var height = right.Height; + + // Setup encoder + compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(width, height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); + output = new DdsFile(ddsHeader, dxt10Header); + + if (OutputOptions.DdsBc1WriteAlphaFlag && + OutputOptions.Format == CompressionFormat.Bc1WithAlpha) + { + output.header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DdpfAlphaPixels; + } + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight); + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // EncodeBlock all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); + + // Encode all mipmap levels per face + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + + if (mip == 0) + { + output.Faces.Add(new DdsFace((uint)mipChain[mip].Width, (uint)mipChain[mip].Height, + (uint)encoded.Length, mipChain.Length)); + } + + output.Faces[face].MipMaps[mip] = new DdsMipMap(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + } + + output.header.dwCaps |= HeaderCaps.DdscapsComplex; + output.header.dwMipMapCount = (uint)numMipMaps; + if (numMipMaps > 1) + { + output.header.dwCaps |= HeaderCaps.DdscapsMipmap; + } + + output.header.dwCaps2 |= HeaderCaps2.Ddscaps2Cubemap | + HeaderCaps2.Ddscaps2CubemapPositivex | + HeaderCaps2.Ddscaps2CubemapNegativex | + HeaderCaps2.Ddscaps2CubemapPositivey | + HeaderCaps2.Ddscaps2CubemapNegativey | + HeaderCaps2.Ddscaps2CubemapPositivez | + HeaderCaps2.Ddscaps2CubemapNegativez; + + return output; + } + + private byte[] EncodeBlockInternalHdr(ReadOnlySpan2D input) + { + var compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder != null) + { + var output = new byte[compressedEncoder.GetBlockSize()]; + + var rawBlock = new RawBlock4X4RgbFloat(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels[4..]); + input.GetRowSpan(2).CopyTo(pixels[8..]); + input.GetRowSpan(3).CopyTo(pixels[12..]); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + return output; + } + + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); + } + + private void EncodeBlockInternalHdr(ReadOnlySpan2D input, Stream outputStream) + { + var compressedEncoder = GetFloatBlockEncoder(OutputOptions.Format); + if (compressedEncoder != null) + { + if (input.Width != 4 || input.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + + Span output = stackalloc byte[16]; + output = output[..compressedEncoder.GetBlockSize()]; + + var rawBlock = new RawBlock4X4RgbFloat(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels[4..]); + input.GetRowSpan(2).CopyTo(pixels[8..]); + input.GetRowSpan(3).CopyTo(pixels[12..]); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + outputStream.Write(output); + } + else + { + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); + } + } + + #endregion + + #region LDR + private void EncodeToStreamInternal(ReadOnlyMemory2D input, Stream outputStream, CancellationToken token) + { + switch (OutputOptions.FileFormat) + { + case OutputFileFormat.Dds: + var dds = EncodeToDdsInternal(input, token); + dds.Write(outputStream); + break; + + case OutputFileFormat.Ktx: + var ktx = EncodeToKtxInternal(input, token); + ktx.Write(outputStream); + break; + } + } + + private KtxFile EncodeToKtxInternal(ReadOnlyMemory2D input, CancellationToken token) + { + KtxFile output; + IBcBlockEncoder compressedEncoder = null; + IRawEncoder uncompressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoders + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) + { + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + output = new KtxFile( + KtxHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetInternalFormat(), + compressedEncoder.GetBaseInternalFormat())); + } + else + { + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + output = new KtxFile( + KtxHeader.InitializeUncompressed(input.Width, input.Height, + uncompressedEncoder.GetGlType(), + uncompressedEncoder.GetGlFormat(), + uncompressedEncoder.GetGlTypeSize(), + uncompressedEncoder.GetInternalFormat(), + uncompressedEncoder.GetBaseInternalFormat())); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = isCompressedFormat ? mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)) : mipChain.Sum(m => m.Width * m.Height); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, + out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, + context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + } + else + { + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => x.Width * x.Height)); + } + + output.MipMaps.Add(new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height, 1)); + output.MipMaps[mip].Faces[0] = new KtxMipFace(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + + output.header.NumberOfFaces = 1; + output.header.NumberOfMipmapLevels = (uint)numMipMaps; + + return output; + } + + private DdsFile EncodeToDdsInternal(ReadOnlyMemory2D input, CancellationToken token) + { + DdsFile output; + IBcBlockEncoder compressedEncoder = null; + IRawEncoder uncompressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) + { + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(input.Width, input.Height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); + output = new DdsFile(ddsHeader, dxt10Header); + + if (OutputOptions.DdsBc1WriteAlphaFlag && + OutputOptions.Format == CompressionFormat.Bc1WithAlpha) + { + output.header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DdpfAlphaPixels; + } + } + else + { + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + var ddsHeader = DdsHeader.InitializeUncompressed(input.Width, input.Height, + uncompressedEncoder.GetDxgiFormat()); + output = new DdsFile(ddsHeader); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = isCompressedFormat ? mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)) : mipChain.Sum(m => m.Width * m.Height); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + } + else + { + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => x.Width * x.Height)); + } + + if (mip == 0) + { + output.Faces.Add(new DdsFace((uint)input.Width, (uint)input.Height, + (uint)encoded.Length, numMipMaps)); + } + + output.Faces[0].MipMaps[mip] = new DdsMipMap(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + + + output.header.dwMipMapCount = (uint)numMipMaps; + if (numMipMaps > 1) + { + output.header.dwCaps |= HeaderCaps.DdscapsComplex | HeaderCaps.DdscapsMipmap; + } + + return output; + } + + private byte[][] EncodeToRawInternal(ReadOnlyMemory2D input, CancellationToken token) + { + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + var output = new byte[numMipMaps][]; + IBcBlockEncoder compressedEncoder = null; + IRawEncoder uncompressedEncoder = null; + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + + if (isCompressedFormat) + { + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + } + else + { + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = isCompressedFormat ? mipChain.Sum(m => ImageToBlocks.CalculateNumOfBlocks(m.Width, m.Height)) : mipChain.Sum(m => m.Width * m.Height); + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode all mipmap levels + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => ImageToBlocks.CalculateNumOfBlocks(x.Width, x.Height))); + } + else + { + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + context.Progress.SetProcessedBlocks(mipChain.Take(mip + 1).Sum(x => x.Width * x.Height)); + } + + output[mip] = encoded; + } + + return output; + } + + private byte[] EncodeToRawInternal(ReadOnlyMemory2D input, int mipLevel, out int mipWidth, out int mipHeight, CancellationToken token) + { + mipLevel = Math.Max(0, mipLevel); + + IBcBlockEncoder compressedEncoder = null; + IRawEncoder uncompressedEncoder = null; + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipChain = MipMapper.GenerateMipChain(input, ref numMipMaps); + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) + { + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + } + else + { + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + } + + // Dispose all mipmap levels + if (mipLevel > numMipMaps - 1) + { + throw new ArgumentException($"{nameof(mipLevel)} cannot be more than number of mipmaps."); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = isCompressedFormat ? ImageToBlocks.CalculateNumOfBlocks(mipChain[mipLevel].Width, mipChain[mipLevel].Height) : mipChain[mipLevel].Width * mipChain[mipLevel].Height; + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode mipmap level + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + } + else + { + if (!mipChain[mipLevel].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + } + + mipWidth = mipChain[mipLevel].Width; + mipHeight = mipChain[mipLevel].Height; + + return encoded; + } + + private void EncodeCubeMapToStreamInternal(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, Stream outputStream, CancellationToken token) + { + switch (OutputOptions.FileFormat) + { + case OutputFileFormat.Ktx: + var ktx = EncodeCubeMapToKtxInternal(right, left, top, down, back, front, token); + ktx.Write(outputStream); + break; + + case OutputFileFormat.Dds: + var dds = EncodeCubeMapToDdsInternal(right, left, top, down, back, front, token); + dds.Write(outputStream); + break; + } + } + + private KtxFile EncodeCubeMapToKtxInternal(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) + { + KtxFile output; + IBcBlockEncoder compressedEncoder = null; + IRawEncoder uncompressedEncoder = null; + + var faces = new[] { right, left, top, down, back, front }; + + var width = right.Width; + var height = right.Height; + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) + { + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + output = new KtxFile( + KtxHeader.InitializeCompressed(width, height, + compressedEncoder.GetInternalFormat(), + compressedEncoder.GetBaseInternalFormat())); + } + else + { + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + output = new KtxFile( + KtxHeader.InitializeUncompressed(width, height, + uncompressedEncoder.GetGlType(), + uncompressedEncoder.GetGlFormat(), + uncompressedEncoder.GetGlTypeSize(), + uncompressedEncoder.GetInternalFormat(), + uncompressedEncoder.GetBaseInternalFormat())); + + } + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + var mipLength = MipMapper.CalculateMipChainLength(width, height, numMipMaps); + for (uint i = 0; i < mipLength; i++) + { + output.MipMaps.Add(new KtxMipmap(0, 0, 0, (uint)faces.Length)); + } + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += isCompressedFormat ? ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight) : mipWidth * mipHeight; + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // Encode all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); + + // Encode all mipmap levels per face + for (var mipLevel = 0; mipLevel < numMipMaps; mipLevel++) + { + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mipLevel], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + } + else + { + if (!mipChain[mipLevel].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + processedBlocks += mipMemory.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + } + + if (face == 0) + { + output.MipMaps[mipLevel] = new KtxMipmap((uint)encoded.Length, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height, (uint)faces.Length); + } + + output.MipMaps[mipLevel].Faces[face] = new KtxMipFace(encoded, + (uint)mipChain[mipLevel].Width, + (uint)mipChain[mipLevel].Height); + } + } + + output.header.NumberOfFaces = (uint)faces.Length; + output.header.NumberOfMipmapLevels = (uint)mipLength; + + return output; + } + + private DdsFile EncodeCubeMapToDdsInternal(ReadOnlyMemory2D right, ReadOnlyMemory2D left, ReadOnlyMemory2D top, ReadOnlyMemory2D down, + ReadOnlyMemory2D back, ReadOnlyMemory2D front, CancellationToken token) + { + DdsFile output; + IBcBlockEncoder compressedEncoder = null; + IRawEncoder uncompressedEncoder = null; + + var faces = new[] { right, left, top, down, back, front }; + + var width = right.Width; + var height = right.Height; + + // Setup encoder + var isCompressedFormat = OutputOptions.Format.IsCompressedFormat(); + if (isCompressedFormat) + { + compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder == null) + { + throw new NotSupportedException($"This Format is not supported: {OutputOptions.Format}"); + } + + var (ddsHeader, dxt10Header) = DdsHeader.InitializeCompressed(width, height, + compressedEncoder.GetDxgiFormat(), OutputOptions.DdsPreferDxt10Header); + output = new DdsFile(ddsHeader, dxt10Header); + + if (OutputOptions.DdsBc1WriteAlphaFlag && + OutputOptions.Format == CompressionFormat.Bc1WithAlpha) + { + output.header.ddsPixelFormat.dwFlags |= PixelFormatFlags.DdpfAlphaPixels; + } + } + else + { + uncompressedEncoder = GetRawEncoder(OutputOptions.Format); + var ddsHeader = DdsHeader.InitializeUncompressed(width, height, + uncompressedEncoder.GetDxgiFormat()); + + output = new DdsFile(ddsHeader); + } + + var numMipMaps = OutputOptions.GenerateMipMaps ? OutputOptions.MaxMipMapLevel : 1; + + var context = new OperationContext + { + CancellationToken = token, + IsParallel = !Debugger.IsAttached && Options.IsParallel, + TaskCount = Options.TaskCount + }; + + // Calculate total blocks + var totalBlocks = 0; + foreach (var face in faces) + { + for (var mip = 0; mip < numMipMaps; mip++) + { + MipMapper.CalculateMipLevelSize(width, height, mip, out var mipWidth, out var mipHeight); + totalBlocks += isCompressedFormat ? ImageToBlocks.CalculateNumOfBlocks(mipWidth, mipHeight) : mipWidth * mipHeight; + } + } + context.Progress = new OperationProgress(Options.Progress, totalBlocks); + + // EncodeBlock all faces + var processedBlocks = 0; + for (var face = 0; face < faces.Length; face++) + { + var mipChain = MipMapper.GenerateMipChain(faces[face], ref numMipMaps); + + // Encode all mipmap levels per face + for (var mip = 0; mip < numMipMaps; mip++) + { + byte[] encoded; + if (isCompressedFormat) + { + var blocks = ImageToBlocks.ImageTo4X4(mipChain[mip], out var blocksWidth, out var blocksHeight); + encoded = compressedEncoder.Encode(blocks, blocksWidth, blocksHeight, OutputOptions.Quality, context); + + processedBlocks += blocks.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + } + else + { + if (!mipChain[mip].TryGetMemory(out var mipMemory)) + { + throw new InvalidOperationException("Could not get Memory from Memory2D."); + } + + encoded = uncompressedEncoder.Encode(mipMemory); + + processedBlocks += mipMemory.Length; + context.Progress.SetProcessedBlocks(processedBlocks); + } + + if (mip == 0) + { + output.Faces.Add(new DdsFace((uint)mipChain[mip].Width, (uint)mipChain[mip].Height, + (uint)encoded.Length, mipChain.Length)); + } + + output.Faces[face].MipMaps[mip] = new DdsMipMap(encoded, + (uint)mipChain[mip].Width, + (uint)mipChain[mip].Height); + } + } + + output.header.dwCaps |= HeaderCaps.DdscapsComplex; + output.header.dwMipMapCount = (uint)numMipMaps; + if (numMipMaps > 1) + { + output.header.dwCaps |= HeaderCaps.DdscapsMipmap; + } + + output.header.dwCaps2 |= HeaderCaps2.Ddscaps2Cubemap | + HeaderCaps2.Ddscaps2CubemapPositivex | + HeaderCaps2.Ddscaps2CubemapNegativex | + HeaderCaps2.Ddscaps2CubemapPositivey | + HeaderCaps2.Ddscaps2CubemapNegativey | + HeaderCaps2.Ddscaps2CubemapPositivez | + HeaderCaps2.Ddscaps2CubemapNegativez; + + return output; + } + + private byte[] EncodeBlockInternal(ReadOnlySpan2D input) + { + var compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder != null) + { + var output = new byte[compressedEncoder.GetBlockSize()]; + + var rawBlock = new RawBlock4X4Rgba32(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels[4..]); + input.GetRowSpan(2).CopyTo(pixels[8..]); + input.GetRowSpan(3).CopyTo(pixels[12..]); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + return output; + } + + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); + } + + private void EncodeBlockInternal(ReadOnlySpan2D input, Stream outputStream) + { + var compressedEncoder = GetRgba32BlockEncoder(OutputOptions.Format); + if (compressedEncoder != null) + { + if (input.Width != 4 || input.Height != 4) + { + throw new ArgumentException($"Single block encoding can only encode blocks of 4x4"); + } + + Span output = stackalloc byte[16]; + output = output[..compressedEncoder.GetBlockSize()]; + + var rawBlock = new RawBlock4X4Rgba32(); + + var pixels = rawBlock.AsSpan; + + input.GetRowSpan(0).CopyTo(pixels); + input.GetRowSpan(1).CopyTo(pixels[4..]); + input.GetRowSpan(2).CopyTo(pixels[8..]); + input.GetRowSpan(3).CopyTo(pixels[12..]); + + compressedEncoder.EncodeBlock(rawBlock, OutputOptions.Quality, output); + + outputStream.Write(output); + } + else + { + throw new NotSupportedException($"This Format is not supported for single block encoding: {OutputOptions.Format}"); + } + } + + #endregion + + #endregion + + #region Support + + private IBcBlockEncoder GetRgba32BlockEncoder(CompressionFormat format) + { + return format switch + { + CompressionFormat.Bc1 => new Bc1BlockEncoder(), + CompressionFormat.Bc1WithAlpha => new Bc1AlphaBlockEncoder(), + CompressionFormat.Bc2 => new Bc2BlockEncoder(), + CompressionFormat.Bc3 => new Bc3BlockEncoder(), + CompressionFormat.Bc4 => new Bc4BlockEncoder(InputOptions.Bc4Component), + CompressionFormat.Bc5 => new Bc5BlockEncoder(InputOptions.Bc5Component1, InputOptions.Bc5Component2), + CompressionFormat.Bc7 => new Bc7Encoder(), + CompressionFormat.Atc => new AtcBlockEncoder(), + CompressionFormat.AtcExplicitAlpha => new AtcExplicitAlphaBlockEncoder(), + CompressionFormat.AtcInterpolatedAlpha => new AtcInterpolatedAlphaBlockEncoder(), + _ => null, + }; + } + + private static Bc6Encoder GetFloatBlockEncoder(CompressionFormat format) + { + return format switch + { + CompressionFormat.Bc6S => new Bc6Encoder(true), + CompressionFormat.Bc6U => new Bc6Encoder(false), + _ => null, + }; + } + + private IRawEncoder GetRawEncoder(CompressionFormat format) + { + return format switch + { + CompressionFormat.R => new RawLuminanceEncoder(InputOptions.LuminanceAsRed), + CompressionFormat.Rg => new RawRgEncoder(), + CompressionFormat.Rgb => new RawRgbEncoder(), + CompressionFormat.Rgba => new RawRgbaEncoder(), + CompressionFormat.Bgra => new RawBgraEncoder(), + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), + }; + } + + private static ReadOnlyMemory2D ByteToColorMemory(ReadOnlySpan span, int width, int height, PixelFormat format) + { + var pixels = new ColorRgba32[width * height]; + + switch (format) + { + case PixelFormat.Rgba32: + for (var i = 0; i < width * height * 4; i += 4) + pixels[i / 4] = new ColorRgba32(span[i], span[i + 1], span[i + 2], span[i + 3]); + break; + + case PixelFormat.Rgb24: + for (var i = 0; i < width * height * 3; i += 3) + pixels[i / 3] = new ColorRgba32(span[i], span[i + 1], span[i + 2], 255); + break; + + case PixelFormat.Bgra32: + for (var i = 0; i < width * height * 4; i += 4) + pixels[i / 4] = new ColorRgba32(span[i + 2], span[i + 1], span[i], span[i + 3]); + break; + + case PixelFormat.Bgr24: + for (var i = 0; i < width * height * 3; i += 3) + pixels[i / 3] = new ColorRgba32(span[i + 2], span[i + 1], span[i], 255); + break; + + case PixelFormat.Argb32: + for (var i = 0; i < width * height * 4; i += 4) + pixels[i / 4] = new ColorRgba32(span[i + 1], span[i + 2], span[i + 3], span[i]); + break; + } + + return new ReadOnlyMemory2D(pixels, height, width); + } + + #endregion + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6Encoder.cs new file mode 100644 index 0000000..e52f76e --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6Encoder.cs @@ -0,0 +1,340 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal class Bc6Encoder : BaseBcBlockEncoder + { + private readonly bool signed; + + public Bc6Encoder(bool signed) + { + this.signed = signed; + } + + + public override GlInternalFormat GetInternalFormat() + { + return signed ? GlInternalFormat.GlCompressedRgbBptcSignedFloatArb : GlInternalFormat.GlCompressedRgbBptcUnsignedFloatArb; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } + + public override DxgiFormat GetDxgiFormat() + { + return signed ? DxgiFormat.DxgiFormatBc6HSf16 : DxgiFormat.DxgiFormatBc6HUf16; + } + + public override Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc6EncoderFast.EncodeBlock(block, signed); + case CompressionQuality.Balanced: + return Bc6EncoderBalanced.EncodeBlock(block, signed); + case CompressionQuality.BestQuality: + return Bc6EncoderBestQuality.EncodeBlock(block, signed); + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + internal static ClusterIndices4X4 CreateClusterIndexBlock(RawBlock4X4RgbFloat raw, out int outputNumClusters, + int numClusters = 2) + { + + var indexBlock = new ClusterIndices4X4(); + + var indices = LinearClustering.ClusterPixels(raw.AsSpan, 4, 4, + numClusters, 1, 10, false); + + var output = indexBlock.AsSpan; + for (var i = 0; i < output.Length; i++) + { + output[i] = indices[i]; + } + + var nClusters = indexBlock.NumClusters; + if (nClusters < numClusters) + { + indexBlock = indexBlock.Reduce(out nClusters); + } + + outputNumClusters = nClusters; + return indexBlock; + } + + internal static class Bc6EncoderFast + { + internal static Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, bool signed) + { + RgbBoundingBox.CreateFloat(block.AsSpan, out var min, out var max); + + LeastSquares.OptimizeEndpoints1Sub(block, ref min, ref max); + + return Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type3, block, min, max, signed, out _); + } + } + + internal static class Bc6EncoderBalanced + { + + private const float TargetError = 0.001f; + private const int MaxTries = 10; + + private static IEnumerable GenerateCandidates(RawBlock4X4RgbFloat block, bool signed) + { + var candidates = 0; + Bc6EncodingHelpers.GetInitialUnscaledEndpoints(block, out var ep0Sub1, out var ep1Sub1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints1Sub(block, ref ep0Sub1, ref ep1Sub1); + } + + ep0Sub1.ClampToHalf(); + ep1Sub1.ClampToHalf(); + + if (!signed) + { + ep0Sub1.ClampToPositive(); + ep1Sub1.ClampToPositive(); + } + + //Type3 Always ok! + yield return Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type3, block, ep0Sub1, ep1Sub1, + signed, out _); + candidates++; + + var type15Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type15, block, ep0Sub1, ep1Sub1, + signed, out var badType15); + candidates++; + if (!badType15) + { + yield return type15Block; + } + else + { + var indexBlock = CreateClusterIndexBlock(block, out var numClusters, 2); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); + + foreach (var subsetPartition in best2SubsetPartitions) + { + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, subsetPartition, 0); + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep2, out var ep3, subsetPartition, 1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints2Sub(block, ref ep0, ref ep1, subsetPartition, 0); + LeastSquares.OptimizeEndpoints2Sub(block, ref ep2, ref ep3, subsetPartition, 1); + } + + ep0.ClampToHalf(); + ep1.ClampToHalf(); + ep2.ClampToHalf(); + ep3.ClampToHalf(); + + if (!signed) + { + ep0.ClampToPositive(); + ep1.ClampToPositive(); + ep2.ClampToPositive(); + ep3.ClampToPositive(); + } + + { + var type1Block = Bc6ModeEncoder.EncodeBlock2Sub(Bc6BlockType.Type1, block, ep0, ep1, ep2, ep3, + subsetPartition, signed, out var badType1); + candidates++; + + if (!badType1) + { + yield return type1Block; + } + + if (candidates >= MaxTries) + { + yield break; + } + } + + { + var type14Block = Bc6ModeEncoder.EncodeBlock2Sub(Bc6BlockType.Type14, block, ep0, ep1, ep2, ep3, + subsetPartition, signed, out var badType14); + candidates++; + + if (!badType14) + { + yield return type14Block; + } + + if (candidates >= MaxTries) + { + yield break; + } + } + } + } + } + + internal static Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, bool signed) + { + var result = new Bc6Block(); + var bestError = 9999999f; + + foreach (var candidate in GenerateCandidates(block, signed)) + { + var error = block.CalculateError(candidate.Decode(signed)); + + if (error < bestError) + { + result = candidate; + bestError = error; + } + + if (error <= TargetError) + { + break; + } + } + + return result; + } + } + + internal static class Bc6EncoderBestQuality + { + private const float TargetError = 0.0005f; + private const int MaxTries = 500; + + private static IEnumerable GenerateCandidates(RawBlock4X4RgbFloat block, bool signed) + { + var candidates = 0; + Bc6EncodingHelpers.GetInitialUnscaledEndpoints(block, out var ep0Sub1, out var ep1Sub1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints1Sub(block, ref ep0Sub1, ref ep1Sub1); + } + + ep0Sub1.ClampToHalf(); + ep1Sub1.ClampToHalf(); + + if (!signed) + { + ep0Sub1.ClampToPositive(); + ep1Sub1.ClampToPositive(); + } + //Type3 Always ok! + yield return Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type3, block, ep0Sub1, ep1Sub1, + signed, out _); + candidates++; + + //Type7 + { + var type7Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type7, block, ep0Sub1, ep1Sub1, + signed, out var badType7); + candidates++; + if (!badType7) + { + yield return type7Block; + } + } + //Type11 + { + var type11Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type11, block, ep0Sub1, ep1Sub1, + signed, out var badType11); + candidates++; + if (!badType11) + { + yield return type11Block; + } + } + //Type15 + { + var type15Block = Bc6ModeEncoder.EncodeBlock1Sub(Bc6BlockType.Type15, block, ep0Sub1, ep1Sub1, + signed, out var badType15); + candidates++; + if (!badType15) + { + yield return type15Block; + } + } + + var indexBlock = CreateClusterIndexBlock(block, out var numClusters, 2); + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock, numClusters, true); + + foreach (var subsetPartition in best2SubsetPartitions) + { + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, subsetPartition, 0); + Bc6EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep2, out var ep3, subsetPartition, 1); + + if (!signed) + { + LeastSquares.OptimizeEndpoints2Sub(block, ref ep0, ref ep1, subsetPartition, 0); + LeastSquares.OptimizeEndpoints2Sub(block, ref ep2, ref ep3, subsetPartition, 1); + } + + ep0.ClampToHalf(); + ep1.ClampToHalf(); + ep2.ClampToHalf(); + ep3.ClampToHalf(); + + if (!signed) + { + ep0.ClampToPositive(); + ep1.ClampToPositive(); + ep2.ClampToPositive(); + ep3.ClampToPositive(); + } + + foreach (var type in Bc6Block.Subsets2Types) + { + var sub2Block = Bc6ModeEncoder.EncodeBlock2Sub(type, block, ep0, ep1, ep2, ep3, + subsetPartition, signed, out var badTransform); + candidates++; + + if (!badTransform) + { + yield return sub2Block; + } + + if (candidates >= MaxTries) + { + yield break; + } + } + } + } + + internal static Bc6Block EncodeBlock(RawBlock4X4RgbFloat block, bool signed) + { + var result = new Bc6Block(); + float bestError = 9999999; + + foreach (var candidate in GenerateCandidates(block, signed)) + { + var error = block.CalculateError(candidate.Decode(signed)); + + if (error < bestError) + { + result = candidate; + bestError = error; + } + + if (error <= TargetError) + { + break; + } + } + + return result; + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6EncodingHelpers.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6EncodingHelpers.cs new file mode 100644 index 0000000..d69ff36 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6EncodingHelpers.cs @@ -0,0 +1,367 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using Half = TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.Half; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc6EncodingHelpers + { + + /// + /// Opposite of + /// + internal static int PreQuantize(float value, bool signed) + { + var half = new Half(value); + var bits = (int)Half.GetBits(half); + if (!signed) + { + + return (bits << 6) / 31; + } + else + { + const int signMask = ~0x8000; + + if (half < new Half(0)) + { + var component = -(bits & signMask); + + return -((-component << 5) / 31); //-(((-component) * 31) >> 5) + } + + return (bits << 5) / 31; + } + } + + /// + /// Opposite of + /// + internal static int Quantize(int component, int endpointBits, bool signed) + { + if (!signed) + { + if (endpointBits >= 15) + return component; + if (component == 0) + return 0; + if (component == 0xFFFF) + return (1 << endpointBits) - 1; + else + return (component << endpointBits) - 0x8000 >> 16; + + } + else + { + if (endpointBits >= 16) + return component; + else + { + if (component == 0) return 0; + if (component > 0) + { + if (component == 0x7FFF) + { + return (1 << endpointBits - 1) - 1; + } + + return (component << endpointBits - 1) - 0x4000 >> 15; + } + else + { + if (-component == 0x7FFF) + { + return -((1 << endpointBits - 1) - 1); + } + + return -((-component << endpointBits - 1) + 0x4000 >> 15); + } + } + } + + } + + public static (int, int, int) PreQuantizeRawEndpoint(ColorRgbFloat endpoint, bool signed) + { + var r = PreQuantize(endpoint.r, signed); + var g = PreQuantize(endpoint.g, signed); + var b = PreQuantize(endpoint.b, signed); + + return ( + r, + g, + b + ); + } + + public static (int, int, int) FinishQuantizeEndpoint((int, int, int) endpoint, int endpointBits, bool signed) + { + return ( + Quantize(endpoint.Item1, endpointBits, signed), + Quantize(endpoint.Item2, endpointBits, signed), + Quantize(endpoint.Item3, endpointBits, signed) + ); + } + + public static int CreateTranformedEndpoint(int quantizedEp0, + int quantizedEpT, int deltaBits, ref bool badTransform) + { + var delta = quantizedEpT - quantizedEp0; + var max = 1 << deltaBits - 1; + + if (delta >= 0 ? delta >= max : -delta > max) // delta overflow + { + badTransform = true; + } + + if (delta >= 0) + { + if (delta >= max) + { + delta = max - 1; + } + } + else if (-delta > max) + { + delta = max; + } + else + { + delta &= (1 << deltaBits) - 1; + } + return delta; + } + + public static (int, int, int) CreateTransformedEndpoint((int, int, int) quantizedEp0, + (int, int, int) quantizedEpT, (int, int, int) deltaBits, ref bool badTransform) + { + return ( + CreateTranformedEndpoint(quantizedEp0.Item1, quantizedEpT.Item1, deltaBits.Item1, ref badTransform), + CreateTranformedEndpoint(quantizedEp0.Item2, quantizedEpT.Item2, deltaBits.Item2, ref badTransform), + CreateTranformedEndpoint(quantizedEp0.Item3, quantizedEpT.Item3, deltaBits.Item3, ref badTransform) + ); + } + + public static void GeneratePalette(Span palette, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, int indexPrecision, bool signed) + { + var paletteSize = 1 << indexPrecision; + + for (var i = 0; i < paletteSize; i++) + { + var interpolated = Bc6Block.InterpolateColor(unQuantizedEp0, unQuantizedEp1, i, indexPrecision); + var (r, g, b) = Bc6Block.FinishUnQuantize(interpolated, signed); + palette[i] = new ColorRgbFloat(r, g, b); + } + } + + public static void GeneratePaletteInt(Span<(int, int, int)> palette, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, int indexPrecision/*, bool signed*/) + { + var paletteSize = 1 << indexPrecision; + + for (var i = 0; i < paletteSize; i++) + { + var interpolated = Bc6Block.InterpolateColor(unQuantizedEp0, unQuantizedEp1, i, indexPrecision); + palette[i] = interpolated; + } + } + + private static int FindClosestColorIndexInt((int, int, int) color, ReadOnlySpan<(int, int, int)> colors, out float bestError) + { + static float CalculateError((int, int, int) c0, (int, int, int) c1) => + Math.Abs(c0.Item1 - c1.Item1) + + Math.Abs(c0.Item2 - c1.Item2) + + Math.Abs(c0.Item3 - c1.Item3); + + bestError = CalculateError(color, colors[0]); + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) + { + var error = CalculateError(color, colors[i]); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + if (bestError == 0) + { + break; + } + } + return bestIndex; + } + + private static int FindClosestColorIndex(ColorRgbFloat color, ReadOnlySpan colors, out float bestError) + { + bestError = color.CalcLogDist(colors[0]); + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) + { + var error = color.CalcLogDist(colors[i]); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + if (bestError == 0) + { + break; + } + } + return bestIndex; + } + + public static float FindOptimalIndicesInt1Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, bool signed) + { + const int paletteSize = 1 << 4; + Span<(int, int, int)> palette = stackalloc (int, int, int)[paletteSize]; + GeneratePaletteInt(palette, unQuantizedEp0, unQuantizedEp1, 4/*, signed*/); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + var intPixel = PreQuantizeRawEndpoint(pixels[i], signed); + indices[i] = (byte)FindClosestColorIndexInt(intPixel, palette, out var e); + error += e; + } + return MathF.Sqrt(error / (3 * 16)); + } + + + public static float FindOptimalIndices1Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, bool signed) + { + const int paletteSize = 1 << 4; + Span palette = stackalloc ColorRgbFloat[paletteSize]; + GeneratePalette(palette, unQuantizedEp0, unQuantizedEp1, 4, signed); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + + indices[i] = (byte)FindClosestColorIndex(pixels[i], palette, out var e); + error += e; + } + return error; + } + + public static float FindOptimalIndicesInt2Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, int partitionSetId, int subsetIndex, bool signed) + { + const int paletteSize = 1 << 3; + Span<(int, int, int)> palette = stackalloc (int, int, int)[paletteSize]; + GeneratePaletteInt(palette, unQuantizedEp0, unQuantizedEp1, 3/*, signed*/); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + var intPixel = PreQuantizeRawEndpoint(pixels[i], signed); + indices[i] = (byte)FindClosestColorIndexInt(intPixel, palette, out var e); + error += e; + } + } + return error; + } + + public static float FindOptimalIndices2Sub(RawBlock4X4RgbFloat block, (int, int, int) unQuantizedEp0, (int, int, int) unQuantizedEp1, + Span indices, int partitionSetId, int subsetIndex, bool signed) + { + const int paletteSize = 1 << 3; + Span palette = stackalloc ColorRgbFloat[paletteSize]; + GeneratePalette(palette, unQuantizedEp0, unQuantizedEp1, 3, signed); + + var pixels = block.AsSpan; + var error = 0f; + for (var i = 0; i < pixels.Length; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + indices[i] = (byte)FindClosestColorIndex(pixels[i], palette, out var e); + error += e; + } + } + return error; + } + + public static void SwapIndicesIfNecessary1Sub(RawBlock4X4RgbFloat block, ref (int, int, int) unQuantizedEp0, ref (int, int, int) unQuantizedEp1, + Span indices, bool signed) + { + const int msb = 1 << 3; + + if ((indices[0] & msb) == 0) + { + return; + } + + InternalUtils.Swap(ref unQuantizedEp0, ref unQuantizedEp1); + FindOptimalIndicesInt1Sub(block, unQuantizedEp0, unQuantizedEp1, indices, signed); + } + + public static void SwapIndicesIfNecessary2Sub(RawBlock4X4RgbFloat block, ref (int, int, int) unQuantizedEp0, ref (int, int, int) unQuantizedEp1, + Span indices, int partitionSetId, int subsetIndex, bool signed) + { + const int msb = 1 << 2; + + var anchorIndex = subsetIndex == 0 ? 0 : Bc6Block.Subsets2AnchorIndices[partitionSetId]; + + if ((indices[anchorIndex] & msb) == 0) + { + return; + } + + InternalUtils.Swap(ref unQuantizedEp0, ref unQuantizedEp1); + FindOptimalIndicesInt2Sub(block, unQuantizedEp0, unQuantizedEp1, indices, partitionSetId, subsetIndex, signed); + } + + public static void GetInitialUnscaledEndpointsForSubset(RawBlock4X4RgbFloat block, out ColorRgbFloat ep0, + out ColorRgbFloat ep1, int partitionSetId, int subsetIndex) + { + + var originalPixels = block.AsSpan; + + var count = 0; + for (var i = 0; i < 16; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + count++; + } + } + + Span subsetColors = stackalloc ColorRgbFloat[count]; + var next = 0; + for (var i = 0; i < 16; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + subsetColors[next++] = originalPixels[i]; + } + } + + PcaVectors.Create(subsetColors, out var mean, out var pa); + PcaVectors.GetExtremePoints(subsetColors, mean, pa, out var min, out var max); + + ep0 = new ColorRgbFloat(min); + ep1 = new ColorRgbFloat(max); + } + + public static void GetInitialUnscaledEndpoints(RawBlock4X4RgbFloat block, out ColorRgbFloat ep0, + out ColorRgbFloat ep1) + { + + var originalPixels = block.AsSpan; + + PcaVectors.Create(originalPixels, out var mean, out var pa); + PcaVectors.GetExtremePoints(originalPixels, mean, pa, out var min, out var max); + + ep0 = new ColorRgbFloat(min); + ep1 = new ColorRgbFloat(max); + } + + + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6ModeEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6ModeEncoder.cs new file mode 100644 index 0000000..15082c9 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc6ModeEncoder.cs @@ -0,0 +1,187 @@ +using System.Diagnostics; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc6ModeEncoder + { + + public static Bc6Block EncodeBlock1Sub(Bc6BlockType type, RawBlock4X4RgbFloat block, ColorRgbFloat initialEndpoint0, + ColorRgbFloat initialEndpoint1, bool signed, out bool badTransform) + { + var endpointBits = type.EndpointBits(); + var deltaBits = type.DeltaBits(); + var hasTransformedEndpoints = type.HasTransformedEndpoints(); + + var initialPreQuantizedEp0 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint0, signed); + var initialPreQuantizedEp1 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint1, signed); + + var initialQuantizedEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp0, endpointBits, signed); + var initialQuantizedEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp1, endpointBits, signed); + + if (hasTransformedEndpoints) + { + // check for delta overflow before index search + var bTransform = false; + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp1, deltaBits, ref bTransform); + if (bTransform) + { + badTransform = true; + return default; + } + } + + var unquantizedEndpoint0 = Bc6Block.UnQuantize(initialQuantizedEp0, endpointBits, signed); + var unquantizedEndpoint1 = Bc6Block.UnQuantize(initialQuantizedEp1, endpointBits, signed); + + Span indices = stackalloc byte[16]; + + Bc6EncodingHelpers.FindOptimalIndicesInt1Sub(block, unquantizedEndpoint0, unquantizedEndpoint1, indices, signed); + + + Bc6EncodingHelpers.SwapIndicesIfNecessary1Sub(block, ref unquantizedEndpoint0, ref unquantizedEndpoint1, indices, signed); + + var quantEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint0, endpointBits, signed); + var quantEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint1, endpointBits, signed); + + badTransform = false; + + if (hasTransformedEndpoints) + { + quantEp1 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp1, deltaBits, + ref badTransform); + } + + switch (type) + { + case Bc6BlockType.Type3: + return Bc6Block.PackType3(quantEp0, quantEp1, indices); + case Bc6BlockType.Type7: + return Bc6Block.PackType7(quantEp0, quantEp1, indices); + case Bc6BlockType.Type11: + return Bc6Block.PackType11(quantEp0, quantEp1, indices); + case Bc6BlockType.Type15: + return Bc6Block.PackType15(quantEp0, quantEp1, indices); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static Bc6Block EncodeBlock2Sub(Bc6BlockType type, RawBlock4X4RgbFloat block, ColorRgbFloat initialEndpoint0, + ColorRgbFloat initialEndpoint1, ColorRgbFloat initialEndpoint2, + ColorRgbFloat initialEndpoint3, int partitionSetId, bool signed, out bool badTransform) + { + Debug.Assert(type.HasSubsets(), "Trying to use 2-subset method for 1-subset block type!"); + + var endpointBits = type.EndpointBits(); + var deltaBits = type.DeltaBits(); + var hasTransformedEndpoints = type.HasTransformedEndpoints(); + + var initialPreQuantizedEp0 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint0, signed); + var initialPreQuantizedEp1 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint1, signed); + var initialPreQuantizedEp2 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint2, signed); + var initialPreQuantizedEp3 = Bc6EncodingHelpers.PreQuantizeRawEndpoint(initialEndpoint3, signed); + + var initialQuantizedEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp0, endpointBits, signed); + var initialQuantizedEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp1, endpointBits, signed); + var initialQuantizedEp2 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp2, endpointBits, signed); + var initialQuantizedEp3 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(initialPreQuantizedEp3, endpointBits, signed); + + if (hasTransformedEndpoints) + { + // check for delta overflow before index search + var bTransform = false; + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp1, deltaBits, ref bTransform); + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp2, deltaBits, ref bTransform); + Bc6EncodingHelpers.CreateTransformedEndpoint(initialQuantizedEp0, initialQuantizedEp3, deltaBits, ref bTransform); + if (bTransform) + { + badTransform = true; + return default; + } + } + + + var unquantizedEndpoint0 = Bc6Block.UnQuantize(initialQuantizedEp0, endpointBits, signed); + var unquantizedEndpoint1 = Bc6Block.UnQuantize(initialQuantizedEp1, endpointBits, signed); + var unquantizedEndpoint2 = Bc6Block.UnQuantize(initialQuantizedEp2, endpointBits, signed); + var unquantizedEndpoint3 = Bc6Block.UnQuantize(initialQuantizedEp3, endpointBits, signed); + + + Span indices = stackalloc byte[16]; + + Bc6EncodingHelpers.FindOptimalIndicesInt2Sub(block, unquantizedEndpoint0, unquantizedEndpoint1, indices, + partitionSetId, 0, signed); + Bc6EncodingHelpers.FindOptimalIndicesInt2Sub(block, unquantizedEndpoint2, unquantizedEndpoint3, indices, + partitionSetId, 1, signed); + + + Bc6EncodingHelpers.SwapIndicesIfNecessary2Sub(block, ref unquantizedEndpoint0, ref unquantizedEndpoint1, indices, + partitionSetId, 0, signed); + Bc6EncodingHelpers.SwapIndicesIfNecessary2Sub(block, ref unquantizedEndpoint2, ref unquantizedEndpoint3, indices, + partitionSetId, 1, signed); + + var quantEp0 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint0, endpointBits, signed); + var quantEp1 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint1, endpointBits, signed); + var quantEp2 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint2, endpointBits, signed); + var quantEp3 = + Bc6EncodingHelpers.FinishQuantizeEndpoint(unquantizedEndpoint3, endpointBits, signed); + + badTransform = false; + + if (hasTransformedEndpoints) + { + quantEp1 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp1, deltaBits, ref badTransform); + quantEp2 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp2, deltaBits, ref badTransform); + quantEp3 = Bc6EncodingHelpers.CreateTransformedEndpoint(quantEp0, quantEp3, deltaBits, ref badTransform); + } + + switch (type) + { + case Bc6BlockType.Type0: + return Bc6Block.PackType0(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type1: + return Bc6Block.PackType1(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type2: + return Bc6Block.PackType2(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type6: + return Bc6Block.PackType6(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type10: + return Bc6Block.PackType10(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type14: + return Bc6Block.PackType14(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type18: + return Bc6Block.PackType18(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type22: + return Bc6Block.PackType22(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type26: + return Bc6Block.PackType26(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + case Bc6BlockType.Type30: + return Bc6Block.PackType30(quantEp0, quantEp1, quantEp2, quantEp3, partitionSetId, + indices); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Encoder.cs new file mode 100644 index 0000000..2f44257 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Encoder.cs @@ -0,0 +1,297 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal class Bc7Encoder : BaseBcBlockEncoder + { + + public override Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock, CompressionQuality quality) + { + switch (quality) + { + case CompressionQuality.Fast: + return Bc7EncoderFast.EncodeBlock(rawBlock); + case CompressionQuality.Balanced: + return Bc7EncoderBalanced.EncodeBlock(rawBlock); + case CompressionQuality.BestQuality: + return Bc7EncoderBestQuality.EncodeBlock(rawBlock); + default: + throw new ArgumentOutOfRangeException(nameof(quality), quality, null); + } + } + + public override GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlCompressedRgbaBptcUnormArb; + } + + public override GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public override DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatBc7Unorm; + } + + private static ClusterIndices4X4 CreateClusterIndexBlock(RawBlock4X4Rgba32 raw, out int outputNumClusters, + int numClusters = 3) + { + + var indexBlock = new ClusterIndices4X4(); + + var indices = LinearClustering.ClusterPixels(raw.AsSpan, 4, 4, + numClusters, 1, 10, false); + + var output = indexBlock.AsSpan; + for (var i = 0; i < output.Length; i++) + { + output[i] = indices[i]; + } + + var nClusters = indexBlock.NumClusters; + if (nClusters < numClusters) + { + indexBlock = indexBlock.Reduce(out nClusters); + } + + outputNumClusters = nClusters; + return indexBlock; + } + + private static class Bc7EncoderFast + { + private const float ErrorThreshold = 0.005f; + private const int MaxTries = 5; + + private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[] best2SubsetPartitions, int[] best3SubsetPartitions, bool alpha) + { + if (alpha) + { + yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 5); + yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 3); + } + else + { + yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 6); + for (var i = 0; i < 64; i++) + { + if (best3SubsetPartitions[i] < 16) + { + yield return Bc7Mode0Encoder.EncodeBlock(rawBlock, 3, best3SubsetPartitions[i]); + } + + yield return Bc7Mode1Encoder.EncodeBlock(rawBlock, 4, best2SubsetPartitions[i]); + + } + } + } + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var hasAlpha = rawBlock.HasTransparentPixels(); + + var indexBlock2 = CreateClusterIndexBlock(rawBlock, out var clusters2, 2); + var indexBlock3 = CreateClusterIndexBlock(rawBlock, out var clusters3, 3); + + if (clusters2 < 2) + { + clusters2 = clusters3; + indexBlock2 = indexBlock3; + } + + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); + var best3SubsetPartitions = BptcEncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); + + float bestError = 99999; + var best = new Bc7Block(); + var tries = 0; + foreach (var block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) + { + var decoded = block.Decode(); + var error = rawBlock.CalculateYCbCrAlphaError(decoded); + tries++; + + if (error < bestError) + { + best = block; + bestError = error; + } + + if (error < ErrorThreshold || tries > MaxTries) + { + break; + } + + } + + return best; + } + } + + private static class Bc7EncoderBalanced + { + private const float ErrorThreshold = 0.005f; + private const int MaxTries = 25; + + private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[] best2SubsetPartitions, int[] best3SubsetPartitions, bool alpha) + { + if (alpha) + { + yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 6); + yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 4); + yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 4); + for (var i = 0; i < 64; i++) + { + yield return Bc7Mode7Encoder.EncodeBlock(rawBlock, 3, best2SubsetPartitions[i]); + } + } + else + { + yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 6); + yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 4); + yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 4); + for (var i = 0; i < 64; i++) + { + if (best3SubsetPartitions[i] < 16) + { + yield return Bc7Mode0Encoder.EncodeBlock(rawBlock, 3, best3SubsetPartitions[i]); + } + else + { + yield return Bc7Mode2Encoder.EncodeBlock(rawBlock, 5, best3SubsetPartitions[i]); + } + + yield return Bc7Mode1Encoder.EncodeBlock(rawBlock, 4, best2SubsetPartitions[i]); + } + } + } + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var hasAlpha = rawBlock.HasTransparentPixels(); + + var indexBlock2 = CreateClusterIndexBlock(rawBlock, out var clusters2, 2); + var indexBlock3 = CreateClusterIndexBlock(rawBlock, out var clusters3, 3); + + if (clusters2 < 2) + { + clusters2 = clusters3; + indexBlock2 = indexBlock3; + } + + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); + var best3SubsetPartitions = BptcEncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); + + float bestError = 99999; + var best = new Bc7Block(); + var tries = 0; + foreach (var block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) + { + var decoded = block.Decode(); + var error = rawBlock.CalculateYCbCrAlphaError(decoded); + tries++; + + if (error < bestError) + { + best = block; + bestError = error; + } + + if (error < ErrorThreshold || tries > MaxTries) + { + break; + } + + } + + return best; + } + } + + private static class Bc7EncoderBestQuality + { + + private const float ErrorThreshold = 0.001f; + private const int MaxTries = 40; + + private static IEnumerable TryMethods(RawBlock4X4Rgba32 rawBlock, int[] best2SubsetPartitions, int[] best3SubsetPartitions, bool alpha) + { + if (alpha) + { + yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 8); + yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 5); + yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 5); + for (var i = 0; i < 64; i++) + { + yield return Bc7Mode7Encoder.EncodeBlock(rawBlock, 4, best2SubsetPartitions[i]); + + } + } + else + { + yield return Bc7Mode6Encoder.EncodeBlock(rawBlock, 8); + yield return Bc7Mode5Encoder.EncodeBlock(rawBlock, 5); + yield return Bc7Mode4Encoder.EncodeBlock(rawBlock, 5); + for (var i = 0; i < 64; i++) + { + if (best3SubsetPartitions[i] < 16) + { + yield return Bc7Mode0Encoder.EncodeBlock(rawBlock, 4, best3SubsetPartitions[i]); + } + yield return Bc7Mode2Encoder.EncodeBlock(rawBlock, 5, best3SubsetPartitions[i]); + + yield return Bc7Mode1Encoder.EncodeBlock(rawBlock, 4, best2SubsetPartitions[i]); + yield return Bc7Mode3Encoder.EncodeBlock(rawBlock, 5, best2SubsetPartitions[i]); + + } + } + } + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 rawBlock) + { + var hasAlpha = rawBlock.HasTransparentPixels(); + + var indexBlock2 = CreateClusterIndexBlock(rawBlock, out var clusters2, 2); + var indexBlock3 = CreateClusterIndexBlock(rawBlock, out var clusters3, 3); + + if (clusters2 < 2) + { + clusters2 = clusters3; + indexBlock2 = indexBlock3; + } + + var best2SubsetPartitions = BptcEncodingHelpers.Rank2SubsetPartitions(indexBlock2, clusters2); + var best3SubsetPartitions = BptcEncodingHelpers.Rank3SubsetPartitions(indexBlock3, clusters3); + + + float bestError = 99999; + var best = new Bc7Block(); + var tries = 0; + foreach (var block in TryMethods(rawBlock, best2SubsetPartitions, best3SubsetPartitions, hasAlpha)) + { + var decoded = block.Decode(); + var error = rawBlock.CalculateYCbCrAlphaError(decoded); + tries++; + + if (error < bestError) + { + best = block; + bestError = error; + } + + if (error < ErrorThreshold || tries > MaxTries) + { + break; + } + + } + + return best; + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7EncodingHelpers.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7EncodingHelpers.cs new file mode 100644 index 0000000..39370d1 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7EncodingHelpers.cs @@ -0,0 +1,701 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + + internal static class Bc7EncodingHelpers + { + private static readonly int[] varPatternRAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0, 0, 0 }; + private static readonly int[] varPatternRNoAlpha = new int[] { 1, -1, 1, 0, 0, -1, 0, 0 }; + + private static readonly int[] varPatternGAlpha = new int[] { 1, -1, 0, 1, 0, 0, -1, 0, 0, 0 }; + private static readonly int[] varPatternGNoAlpha = new int[] { 1, -1, 0, 1, 0, 0, -1, 0 }; + + private static readonly int[] varPatternBAlpha = new int[] { 1, -1, 0, 0, 1, 0, 0, -1, 0, 0 }; + private static readonly int[] varPatternBNoAlpha = new int[] { 1, -1, 0, 0, 1, 0, 0, -1 }; + + private static readonly int[] varPatternAAlpha = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 1, -1 }; + private static readonly int[] varPatternANoAlpha = new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }; + + public static bool TypeHasPBits(Bc7BlockType type) => type switch + { + Bc7BlockType.Type0 => true, + Bc7BlockType.Type1 => true, + Bc7BlockType.Type3 => true, + Bc7BlockType.Type6 => true, + Bc7BlockType.Type7 => true, + _ => false + }; + + public static bool TypeHasSharedPBits(Bc7BlockType type) => type switch + { + Bc7BlockType.Type1 => true, + _ => false + }; + + /// + /// Includes PBit + /// + public static int GetColorComponentPrecisionWithPBit(Bc7BlockType type) => type switch + { + Bc7BlockType.Type0 => 5, + Bc7BlockType.Type1 => 7, + Bc7BlockType.Type2 => 5, + Bc7BlockType.Type3 => 8, + Bc7BlockType.Type4 => 5, + Bc7BlockType.Type5 => 7, + Bc7BlockType.Type6 => 8, + Bc7BlockType.Type7 => 6, + _ => 0 + }; + + /// + /// Includes PBit + /// + public static int GetAlphaComponentPrecisionWithPBit(Bc7BlockType type) => type switch + { + + Bc7BlockType.Type4 => 6, + Bc7BlockType.Type5 => 8, + Bc7BlockType.Type6 => 8, + Bc7BlockType.Type7 => 6, + _ => 0 + }; + + /// + /// Does not include pBit + /// + public static int GetColorComponentPrecision(Bc7BlockType type) => type switch + { + Bc7BlockType.Type0 => 4, + Bc7BlockType.Type1 => 6, + Bc7BlockType.Type2 => 5, + Bc7BlockType.Type3 => 7, + Bc7BlockType.Type4 => 5, + Bc7BlockType.Type5 => 7, + Bc7BlockType.Type6 => 7, + Bc7BlockType.Type7 => 5, + _ => 0 + }; + + /// + /// Does not include pBit + /// + public static int GetAlphaComponentPrecision(Bc7BlockType type) => type switch + { + + Bc7BlockType.Type4 => 6, + Bc7BlockType.Type5 => 8, + Bc7BlockType.Type6 => 7, + Bc7BlockType.Type7 => 5, + _ => 0 + }; + + public static int GetColorIndexBitCount(Bc7BlockType type, int type4IdxMode = 0) => type switch + { + Bc7BlockType.Type0 => 3, + Bc7BlockType.Type1 => 3, + Bc7BlockType.Type2 => 2, + Bc7BlockType.Type3 => 2, + Bc7BlockType.Type4 when type4IdxMode == 0 => 2, + Bc7BlockType.Type4 when type4IdxMode == 1 => 3, + Bc7BlockType.Type5 => 2, + Bc7BlockType.Type6 => 4, + Bc7BlockType.Type7 => 2, + _ => 0 + }; + + public static int GetAlphaIndexBitCount(Bc7BlockType type, int type4IdxMode = 0) => type switch + { + Bc7BlockType.Type4 when type4IdxMode == 0 => 3, + Bc7BlockType.Type4 when type4IdxMode == 1 => 2, + Bc7BlockType.Type5 => 2, + Bc7BlockType.Type6 => 4, + Bc7BlockType.Type7 => 2, + _ => 0 + }; + + + public static void ExpandEndpoints(Bc7BlockType type, ColorRgba32[] endpoints, byte[] pBits) + { + if (type == Bc7BlockType.Type0 || type == Bc7BlockType.Type1 || type == Bc7BlockType.Type3 || type == Bc7BlockType.Type6 || type == Bc7BlockType.Type7) + { + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] <<= 1; + } + + if (type == Bc7BlockType.Type1) + { + endpoints[0] |= pBits[0]; + endpoints[1] |= pBits[0]; + endpoints[2] |= pBits[1]; + endpoints[3] |= pBits[1]; + } + else + { + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] |= pBits[i]; + } + } + } + + var colorPrecision = GetColorComponentPrecisionWithPBit(type); + var alphaPrecision = GetAlphaComponentPrecisionWithPBit(type); + for (var i = 0; i < endpoints.Length; i++) + { + // ColorComponentPrecision & AlphaComponentPrecision includes pbit + // left shift endpoint components so that their MSB lies in bit 7 + endpoints[i].r = (byte)(endpoints[i].r << 8 - colorPrecision); + endpoints[i].g = (byte)(endpoints[i].g << 8 - colorPrecision); + endpoints[i].b = (byte)(endpoints[i].b << 8 - colorPrecision); + endpoints[i].a = (byte)(endpoints[i].a << 8 - alphaPrecision); + + // Replicate each component's MSB into the LSBs revealed by the left-shift operation above + endpoints[i].r = (byte)(endpoints[i].r | endpoints[i].r >> colorPrecision); + endpoints[i].g = (byte)(endpoints[i].g | endpoints[i].g >> colorPrecision); + endpoints[i].b = (byte)(endpoints[i].b | endpoints[i].b >> colorPrecision); + endpoints[i].a = (byte)(endpoints[i].a | endpoints[i].a >> alphaPrecision); + } + + //If this mode does not explicitly define the alpha component + //set alpha equal to 255 + if (type == Bc7BlockType.Type0 || type == Bc7BlockType.Type1 || type == Bc7BlockType.Type2 || type == Bc7BlockType.Type3) + { + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i].a = 255; + } + } + } + + + public static ColorRgba32 ExpandEndpoint(Bc7BlockType type, ColorRgba32 endpoint, byte pBit) + { + if (type == Bc7BlockType.Type0 || type == Bc7BlockType.Type1 || type == Bc7BlockType.Type3 || type == Bc7BlockType.Type6 || type == Bc7BlockType.Type7) + { + endpoint <<= 1; + endpoint |= pBit; + } + + var colorPrecision = GetColorComponentPrecisionWithPBit(type); + var alphaPrecision = GetAlphaComponentPrecisionWithPBit(type); + endpoint.r = (byte)(endpoint.r << 8 - colorPrecision); + endpoint.g = (byte)(endpoint.g << 8 - colorPrecision); + endpoint.b = (byte)(endpoint.b << 8 - colorPrecision); + endpoint.a = (byte)(endpoint.a << 8 - alphaPrecision); + + // Replicate each component's MSB into the LSBs revealed by the left-shift operation above + endpoint.r = (byte)(endpoint.r | endpoint.r >> colorPrecision); + endpoint.g = (byte)(endpoint.g | endpoint.g >> colorPrecision); + endpoint.b = (byte)(endpoint.b | endpoint.b >> colorPrecision); + endpoint.a = (byte)(endpoint.a | endpoint.a >> alphaPrecision); + + //If this mode does not explicitly define the alpha component + //set alpha equal to 255 + if (type == Bc7BlockType.Type0 || type == Bc7BlockType.Type1 || type == Bc7BlockType.Type2 || type == Bc7BlockType.Type3) + { + endpoint.a = 255; + } + + return endpoint; + } + + + public static void GetInitialUnscaledEndpoints(RawBlock4X4Rgba32 block, out ColorRgba32 ep0, + out ColorRgba32 ep1) + { + + var originalPixels = block.AsSpan; + PcaVectors.CreateWithAlpha(originalPixels, out var mean, out var pa); + PcaVectors.GetExtremePointsWithAlpha(block.AsSpan, mean, pa, out var min, out var max); + + ep0 = new ColorRgba32((byte)(min.X * 255), (byte)(min.Y * 255), (byte)(min.Z * 255), (byte)(min.W * 255)); + ep1 = new ColorRgba32((byte)(max.X * 255), (byte)(max.Y * 255), (byte)(max.Z * 255), (byte)(max.W * 255)); + } + + public static void GetInitialUnscaledEndpointsForSubset(RawBlock4X4Rgba32 block, out ColorRgba32 ep0, + out ColorRgba32 ep1, ReadOnlySpan partitionTable, int subsetIndex) + { + + var originalPixels = block.AsSpan; + + var count = 0; + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == subsetIndex) + { + count++; + } + } + + Span subsetColors = stackalloc ColorRgba32[count]; + var next = 0; + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == subsetIndex) + { + subsetColors[next++] = originalPixels[i]; + } + } + + PcaVectors.CreateWithAlpha(subsetColors, out var mean, out var pa); + PcaVectors.GetExtremePointsWithAlpha(block.AsSpan, mean, pa, out var min, out var max); + + ep0 = new ColorRgba32((byte)(min.X * 255), (byte)(min.Y * 255), (byte)(min.Z * 255), (byte)(min.W * 255)); + ep1 = new ColorRgba32((byte)(max.X * 255), (byte)(max.Y * 255), (byte)(max.Z * 255), (byte)(max.W * 255)); + } + + public static ColorRgba32 ScaleDownEndpoint(ColorRgba32 endpoint, Bc7BlockType type, bool ignoreAlpha, out byte pBit) + { + var colorPrecision = GetColorComponentPrecisionWithPBit(type); + var alphaPrecision = GetAlphaComponentPrecisionWithPBit(type); + + var r = (byte)(endpoint.r >> 8 - colorPrecision); + var g = (byte)(endpoint.g >> 8 - colorPrecision); + var b = (byte)(endpoint.b >> 8 - colorPrecision); + var a = (byte)(endpoint.a >> 8 - alphaPrecision); + + if (TypeHasPBits(type)) + { + var pBitVotingMask = (1 << 8 - colorPrecision + 1) - 1; + float pBitVotes = 0; + pBitVotes += endpoint.r & pBitVotingMask; + pBitVotes += endpoint.g & pBitVotingMask; + pBitVotes += endpoint.b & pBitVotingMask; + pBitVotes /= 3; + + if (pBitVotes >= pBitVotingMask / 2f) + { + pBit = 1; + } + else + { + pBit = 0; + } + + r >>= 1; + g >>= 1; + b >>= 1; + a >>= 1; + } + else + { + pBit = 0; + } + + if (ignoreAlpha) + { + return new ColorRgba32(r, g, b, 0); + } + else + { + return new ColorRgba32(r, g, b, a); + } + } + + public static ColorRgba32 InterpolateColor(ColorRgba32 endPointStart, ColorRgba32 endPointEnd, + int colorIndex, int alphaIndex, int colorBitCount, int alphaBitCount) + { + + var result = new ColorRgba32( + BptcEncodingHelpers.InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount) + ); + + return result; + } + + public static void ClampEndpoint(ref ColorRgba32 endpoint, byte colorMax, byte alphaMax) + { + if (endpoint.r > colorMax) endpoint.r = colorMax; + if (endpoint.g > colorMax) endpoint.g = colorMax; + if (endpoint.b > colorMax) endpoint.b = colorMax; + if (endpoint.a > alphaMax) endpoint.a = alphaMax; + } + + private static int FindClosestColorIndex(ColorYCbCrAlpha color, ReadOnlySpan colors, out float bestError) + { + bestError = color.CalcDistWeighted(colors[0], 4, 2); + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) + { + var error = color.CalcDistWeighted(colors[i], 4, 2); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + } + return bestIndex; + } + + private static int FindClosestColorIndex(ColorYCbCr color, ReadOnlySpan colors, out float bestError) + { + bestError = color.CalcDistWeighted(colors[0], 4); + var bestIndex = 0; + for (var i = 1; i < colors.Length; i++) + { + var error = color.CalcDistWeighted(colors[i], 4); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + if (bestError == 0) + { + break; + } + } + return bestIndex; + } + + private static int FindClosestAlphaIndex(byte alpha, ReadOnlySpan alphas, out float bestError) + { + bestError = (alpha - alphas[0]) * (alpha - alphas[0]); + var bestIndex = 0; + for (var i = 1; i < alphas.Length; i++) + { + float error = (alpha - alphas[i]) * (alpha - alphas[i]); + if (error < bestError) + { + bestIndex = i; + bestError = error; + } + + if (bestError == 0) + { + break; + } + } + return bestIndex; + } + + + private static float TrySubsetEndpoints(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1, + ReadOnlySpan partitionTable, int subsetIndex, int type4IdxMode) + { + var colorIndexPrecision = GetColorIndexBitCount(type, type4IdxMode); + var alphaIndexPrecision = GetAlphaIndexBitCount(type, type4IdxMode); + + if (type == Bc7BlockType.Type4 || type == Bc7BlockType.Type5) + { //separate indices for color and alpha + Span colors = stackalloc ColorYCbCr[1 << colorIndexPrecision]; + Span alphas = stackalloc byte[1 << alphaIndexPrecision]; + + for (var i = 0; i < colors.Length; i++) + { + colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, i, + 0, colorIndexPrecision, 0)); + } + + for (var i = 0; i < alphas.Length; i++) + { + alphas[i] = InterpolateColor(ep0, ep1, 0, + i, 0, alphaIndexPrecision).a; + } + + var pixels = raw.AsSpan; + float error = 0; + + for (var i = 0; i < 16; i++) + { + var pixelColor = new ColorYCbCr(pixels[i]); + + FindClosestColorIndex(pixelColor, colors, out var ce); + FindClosestAlphaIndex(pixels[i].a, alphas, out var ae); + + error += ce + ae; + } + + return error / 16; + } + else + { + Span colors = stackalloc ColorYCbCrAlpha[1 << colorIndexPrecision]; + for (var i = 0; i < colors.Length; i++) + { + colors[i] = new ColorYCbCrAlpha(InterpolateColor(ep0, ep1, i, + i, colorIndexPrecision, alphaIndexPrecision)); + } + + var pixels = raw.AsSpan; + float error = 0; + float count = 0; + + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == subsetIndex) + { + var pixelColor = new ColorYCbCrAlpha(pixels[i]); + + FindClosestColorIndex(pixelColor, colors, out var e); + error += e * e; + count++; + } + } + + error /= count; + return error; + } + + } + + public static void FillSubsetIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1, ReadOnlySpan partitionTable, int subsetIndex, + Span indicesToFill) + { + var colorIndexPrecision = GetColorIndexBitCount(type); + var alphaIndexPrecision = GetAlphaIndexBitCount(type); + + if (type == Bc7BlockType.Type4 || type == Bc7BlockType.Type5) + { //separate indices for color and alpha + throw new ArgumentException(); + } + else + { + Span colors = stackalloc ColorYCbCrAlpha[1 << colorIndexPrecision]; + for (var i = 0; i < colors.Length; i++) + { + colors[i] = new ColorYCbCrAlpha(InterpolateColor(ep0, ep1, i, + i, colorIndexPrecision, alphaIndexPrecision)); + } + + var pixels = raw.AsSpan; + + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == subsetIndex) + { + var pixelColor = new ColorYCbCrAlpha(pixels[i]); + + var index = FindClosestColorIndex(pixelColor, colors, out var e); + indicesToFill[i] = (byte)index; + } + } + } + } + + /// + /// Used for Modes 4 and 5 + /// + public static void FillAlphaColorIndices(Bc7BlockType type, RawBlock4X4Rgba32 raw, ColorRgba32 ep0, ColorRgba32 ep1, + Span colorIndicesToFill, Span alphaIndicesToFill, int idxMode = 0) + { + var colorIndexPrecision = GetColorIndexBitCount(type, idxMode); + var alphaIndexPrecision = GetAlphaIndexBitCount(type, idxMode); + + if (type == Bc7BlockType.Type4 || type == Bc7BlockType.Type5) + { + Span colors = stackalloc ColorYCbCr[1 << colorIndexPrecision]; + Span alphas = stackalloc byte[1 << alphaIndexPrecision]; + + for (var i = 0; i < colors.Length; i++) + { + colors[i] = new ColorYCbCr(InterpolateColor(ep0, ep1, i, + 0, colorIndexPrecision, 0)); + } + + for (var i = 0; i < alphas.Length; i++) + { + alphas[i] = InterpolateColor(ep0, ep1, 0, + i, 0, alphaIndexPrecision).a; + } + + var pixels = raw.AsSpan; + + for (var i = 0; i < 16; i++) + { + var pixelColor = new ColorYCbCr(pixels[i]); + + var index = FindClosestColorIndex(pixelColor, colors, out _); + colorIndicesToFill[i] = (byte)index; + + index = FindClosestAlphaIndex(pixels[i].a, alphas, out _); + alphaIndicesToFill[i] = (byte)index; + } + } + else + { + throw new ArgumentException(); + } + } + + public static void OptimizeSubsetEndpointsWithPBit(Bc7BlockType type, RawBlock4X4Rgba32 raw, ref ColorRgba32 ep0, ref ColorRgba32 ep1, ref byte pBit0, ref byte pBit1, + int variation, ReadOnlySpan partitionTable, int subsetIndex, bool variatePBits, bool variateAlpha, int type4IdxMode = 0) + { + + var colorMax = (byte)((1 << GetColorComponentPrecision(type)) - 1); + var alphaMax = (byte)((1 << GetAlphaComponentPrecision(type)) - 1); + + var bestError = TrySubsetEndpoints(type, raw, + ExpandEndpoint(type, ep0, pBit0), + ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode + ); + + ReadOnlySpan patternR = variateAlpha + ? varPatternRAlpha + : varPatternRNoAlpha; + ReadOnlySpan patternG = variateAlpha + ? varPatternGAlpha + : varPatternGNoAlpha; + ReadOnlySpan patternB = variateAlpha + ? varPatternBAlpha + : varPatternBNoAlpha; + ReadOnlySpan patternA = variateAlpha + ? varPatternAAlpha + : varPatternANoAlpha; + + + while (variation > 0) + { + var foundBetter = false; + + for (var i = 0; i < patternR.Length; i++) + { + var testEndPoint0 = new ColorRgba32( + (byte)(ep0.r - variation * patternR[i]), + (byte)(ep0.g - variation * patternG[i]), + (byte)(ep0.b - variation * patternB[i]), + (byte)(ep0.a - variation * patternA[i]) + ); + + var testEndPoint1 = new ColorRgba32( + (byte)(ep1.r + variation * patternR[i]), + (byte)(ep1.g + variation * patternG[i]), + (byte)(ep1.b + variation * patternB[i]), + (byte)(ep1.a + variation * patternA[i]) + ); + ClampEndpoint(ref testEndPoint0, colorMax, alphaMax); + ClampEndpoint(ref testEndPoint1, colorMax, alphaMax); + + var error = TrySubsetEndpoints(type, raw, + ExpandEndpoint(type, testEndPoint0, pBit0), + ExpandEndpoint(type, testEndPoint1, pBit1), partitionTable, subsetIndex, type4IdxMode + ); + if (error < bestError) + { + bestError = error; + ep0 = testEndPoint0; + ep1 = testEndPoint1; + foundBetter = true; + } + } + + for (var i = 0; i < patternR.Length; i++) + { + var testEndPoint0 = new ColorRgba32( + (byte)(ep0.r + variation * patternR[i]), + (byte)(ep0.g + variation * patternG[i]), + (byte)(ep0.b + variation * patternB[i]), + (byte)(ep0.a + variation * patternA[i]) + ); + ClampEndpoint(ref testEndPoint0, colorMax, alphaMax); + + var error = TrySubsetEndpoints(type, raw, + ExpandEndpoint(type, testEndPoint0, pBit0), + ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode + ); + if (error < bestError) + { + bestError = error; + ep0 = testEndPoint0; + foundBetter = true; + } + } + + for (var i = 0; i < patternR.Length; i++) + { + var testEndPoint1 = new ColorRgba32( + (byte)(ep1.r + variation * patternR[i]), + (byte)(ep1.g + variation * patternG[i]), + (byte)(ep1.b + variation * patternB[i]), + (byte)(ep1.a + variation * patternA[i]) + ); + ClampEndpoint(ref testEndPoint1, colorMax, alphaMax); + + var error = TrySubsetEndpoints(type, raw, + ExpandEndpoint(type, ep0, pBit0), + ExpandEndpoint(type, testEndPoint1, pBit1), partitionTable, subsetIndex, type4IdxMode + ); + if (error < bestError) + { + bestError = error; + ep1 = testEndPoint1; + foundBetter = true; + } + } + + if (variatePBits) + { + { + var testPBit0 = pBit0 == 0 ? (byte)1 : (byte)0; + var error = TrySubsetEndpoints(type, raw, + ExpandEndpoint(type, ep0, testPBit0), + ExpandEndpoint(type, ep1, pBit1), partitionTable, subsetIndex, type4IdxMode + ); + if (error < bestError) + { + bestError = error; + pBit0 = testPBit0; + foundBetter = true; + } + } + { + var testPBit1 = pBit1 == 0 ? (byte)1 : (byte)0; + var error = TrySubsetEndpoints(type, raw, + ExpandEndpoint(type, ep0, pBit0), + ExpandEndpoint(type, ep1, testPBit1), partitionTable, subsetIndex, type4IdxMode + ); + if (error < bestError) + { + bestError = error; + pBit1 = testPBit1; + foundBetter = true; + } + } + } + if (!foundBetter) + { + variation--; + } + } + } + + public static RawBlock4X4Rgba32 RotateBlockColors(RawBlock4X4Rgba32 block, int rotation) + { + if (rotation == 0) + { + return block; + } + + var rotated = new RawBlock4X4Rgba32(); + var pixels = block.AsSpan; + var output = rotated.AsSpan; + for (var i = 0; i < 16; i++) + { + var c = pixels[i]; + switch (rotation) + { + case 1: + output[i] = new ColorRgba32(c.a, c.g, c.b, c.r); + break; + case 2: + output[i] = new ColorRgba32(c.r, c.a, c.b, c.g); + break; + case 3: + output[i] = new ColorRgba32(c.r, c.g, c.a, c.b); + break; + } + } + + return rotated; + } + + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode0Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode0Encoder.cs new file mode 100644 index 0000000..1adf7ae --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode0Encoder.cs @@ -0,0 +1,89 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode0Encoder + { + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) + { + var output = new Bc7Block(); + const Bc7BlockType type = Bc7BlockType.Type0; + + if (bestPartition >= 16) + { + throw new IndexOutOfRangeException("Mode0 only has 16 partitions"); + } + + var endpoints = new ColorRgba32[6]; + var pBits = new byte[6]; + ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[bestPartition]; + + var indices = new byte[16]; + + var anchorIndices = new int[] { + 0, + Bc7Block.Subsets3AnchorIndices2[bestPartition], + Bc7Block.Subsets3AnchorIndices3[bestPartition] + }; + + for (var subset = 0; subset < 3; subset++) + { + + Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, + partitionTable, subset); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out var pBit1); + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, + ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, true, false); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + + if ((indices[anchorIndices[subset]] & 0b100) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + var p = pBit0; + + scaledEp0 = scaledEp1; + pBit0 = pBit1; + scaledEp1 = c; + pBit1 = p; + + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + } + + endpoints[subset * 2] = scaledEp0; + endpoints[subset * 2 + 1] = scaledEp1; + pBits[subset * 2] = pBit0; + pBits[subset * 2 + 1] = pBit1; + } + + output.PackType0(bestPartition, new[]{ + new byte[]{endpoints[0].r, endpoints[0].g, endpoints[0].b}, + new byte[]{endpoints[1].r, endpoints[1].g, endpoints[1].b}, + new byte[]{endpoints[2].r, endpoints[2].g, endpoints[2].b}, + new byte[]{endpoints[3].r, endpoints[3].g, endpoints[3].b}, + new byte[]{endpoints[4].r, endpoints[4].g, endpoints[4].b}, + new byte[]{endpoints[5].r, endpoints[5].g, endpoints[5].b} + }, + pBits, + indices); + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode1Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode1Encoder.cs new file mode 100644 index 0000000..784b74e --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode1Encoder.cs @@ -0,0 +1,78 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode1Encoder + { + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) + { + var output = new Bc7Block(); + const Bc7BlockType type = Bc7BlockType.Type1; + + + var endpoints = new ColorRgba32[4]; + var pBits = new byte[2]; + ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[bestPartition]; + + var indices = new byte[16]; + + var anchorIndices = new int[] { + 0, + Bc7Block.Subsets2AnchorIndices[bestPartition] + }; + + for (var subset = 0; subset < 2; subset++) + { + + Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, + partitionTable, subset); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var pBit); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out pBit); + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, + ref scaledEp1, ref pBit, ref pBit, startingVariation, partitionTable, subset, true, false); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + + if ((indices[anchorIndices[subset]] & 0b100) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + + scaledEp0 = scaledEp1; + scaledEp1 = c; + + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + } + + endpoints[subset * 2] = scaledEp0; + endpoints[subset * 2 + 1] = scaledEp1; + pBits[subset] = pBit; + } + + output.PackType1(bestPartition, new[]{ + new byte[]{endpoints[0].r, endpoints[0].g, endpoints[0].b}, + new byte[]{endpoints[1].r, endpoints[1].g, endpoints[1].b}, + new byte[]{endpoints[2].r, endpoints[2].g, endpoints[2].b}, + new byte[]{endpoints[3].r, endpoints[3].g, endpoints[3].b} + }, + pBits, + indices); + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode2Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode2Encoder.cs new file mode 100644 index 0000000..f51f717 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode2Encoder.cs @@ -0,0 +1,78 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode2Encoder + { + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) + { + var output = new Bc7Block(); + const Bc7BlockType type = Bc7BlockType.Type2; + + var endpoints = new ColorRgba32[6]; + ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[bestPartition]; + + var indices = new byte[16]; + + var anchorIndices = new int[] { + 0, + Bc7Block.Subsets3AnchorIndices2[bestPartition], + Bc7Block.Subsets3AnchorIndices3[bestPartition] + }; + + for (var subset = 0; subset < 3; subset++) + { + + Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, + partitionTable, subset); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var _); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out var _); + + byte pBit = 0; + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, + ref scaledEp1, ref pBit, ref pBit, startingVariation, partitionTable, subset, false, false); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + + if ((indices[anchorIndices[subset]] & 0b10) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + + scaledEp0 = scaledEp1; + scaledEp1 = c; + + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + } + + endpoints[subset * 2] = scaledEp0; + endpoints[subset * 2 + 1] = scaledEp1; + } + + output.PackType2(bestPartition, new[]{ + new byte[]{endpoints[0].r, endpoints[0].g, endpoints[0].b}, + new byte[]{endpoints[1].r, endpoints[1].g, endpoints[1].b}, + new byte[]{endpoints[2].r, endpoints[2].g, endpoints[2].b}, + new byte[]{endpoints[3].r, endpoints[3].g, endpoints[3].b}, + new byte[]{endpoints[4].r, endpoints[4].g, endpoints[4].b}, + new byte[]{endpoints[5].r, endpoints[5].g, endpoints[5].b} + }, + indices); + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode3Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode3Encoder.cs new file mode 100644 index 0000000..837ca20 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode3Encoder.cs @@ -0,0 +1,81 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode3Encoder + { + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) + { + var output = new Bc7Block(); + const Bc7BlockType type = Bc7BlockType.Type3; + + var endpoints = new ColorRgba32[4]; + var pBits = new byte[4]; + ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[bestPartition]; + + var indices = new byte[16]; + + var anchorIndices = new int[] { + 0, + Bc7Block.Subsets2AnchorIndices[bestPartition] + }; + + for (var subset = 0; subset < 2; subset++) + { + + Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, + partitionTable, subset); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, true, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, true, out var pBit1); + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, + ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, true, false); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + + if ((indices[anchorIndices[subset]] & 0b10) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + var p = pBit0; + + scaledEp0 = scaledEp1; + pBit0 = pBit1; + scaledEp1 = c; + pBit1 = p; + + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + } + + endpoints[subset * 2] = scaledEp0; + endpoints[subset * 2 + 1] = scaledEp1; + pBits[subset * 2] = pBit0; + pBits[subset * 2 + 1] = pBit1; + } + + output.PackType3(bestPartition, new[]{ + new byte[]{endpoints[0].r, endpoints[0].g, endpoints[0].b}, + new byte[]{endpoints[1].r, endpoints[1].g, endpoints[1].b}, + new byte[]{endpoints[2].r, endpoints[2].g, endpoints[2].b}, + new byte[]{endpoints[3].r, endpoints[3].g, endpoints[3].b} + }, + pBits, + indices); + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode4Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode4Encoder.cs new file mode 100644 index 0000000..91636bf --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode4Encoder.cs @@ -0,0 +1,128 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode4Encoder + { + + private static ReadOnlySpan PartitionTable => new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + const int Subset = 0; + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation) + { + var type = Bc7BlockType.Type4; + + Span outputs = stackalloc Bc7Block[8]; + + for (var idxMode = 0; idxMode < 2; idxMode++) + { + for (var rotation = 0; rotation < 4; rotation++) + { + var rotatedBlock = Bc7EncodingHelpers.RotateBlockColors(block, rotation); + var output = new Bc7Block(); + + Bc7EncodingHelpers.GetInitialUnscaledEndpoints(rotatedBlock, out var ep0, out var ep1); + + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out var _); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out var _); + + byte pBit = 0; //fake pBit + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, rotatedBlock, ref scaledEp0, + ref scaledEp1, ref pBit, ref pBit, startingVariation, PartitionTable, Subset, + false, true, idxMode); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); + var colorIndices = new byte[16]; + var alphaIndices = new byte[16]; + Bc7EncodingHelpers.FillAlphaColorIndices(type, rotatedBlock, + ep0, + ep1, + colorIndices, alphaIndices, idxMode); + + var needsRedo = false; + + + if ((colorIndices[0] & (idxMode == 0 ? 0b10 : 0b100)) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + var alpha0 = scaledEp0.a; + var alpha1 = scaledEp1.a; + + scaledEp0 = scaledEp1; + scaledEp1 = c; + scaledEp0.a = alpha0; + scaledEp1.a = alpha1; + + needsRedo = true; + } + if ((alphaIndices[0] & (idxMode == 0 ? 0b100 : 0b10)) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var a = scaledEp0.a; + + scaledEp0.a = scaledEp1.a; + scaledEp1.a = a; + + needsRedo = true; + } + + if (needsRedo) + { + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); + Bc7EncodingHelpers.FillAlphaColorIndices(type, rotatedBlock, + ep0, + ep1, + colorIndices, alphaIndices, idxMode); + } + + if (idxMode == 0) + { + output.PackType4(rotation, (byte)idxMode, new[]{ + new byte[]{scaledEp0.r, scaledEp0.g, scaledEp0.b}, + new byte[]{scaledEp1.r, scaledEp1.g, scaledEp1.b}, + }, + new[] { scaledEp0.a, scaledEp1.a }, + colorIndices, alphaIndices); + } + else + { + output.PackType4(rotation, (byte)idxMode, new[]{ + new byte[]{scaledEp0.r, scaledEp0.g, scaledEp0.b}, + new byte[]{scaledEp1.r, scaledEp1.g, scaledEp1.b}, + }, + new[] { scaledEp0.a, scaledEp1.a }, + alphaIndices, colorIndices); + } + + + outputs[idxMode * 4 + rotation] = output; + } + } + + var bestIndex = 0; + float bestError = 0; + var first = true; + + // Find best out of generated blocks + for (var i = 0; i < outputs.Length; i++) + { + var decoded = outputs[i].Decode(); + + var error = block.CalculateYCbCrAlphaError(decoded); + if (error < bestError || first) + { + first = false; + bestError = error; + bestIndex = i; + } + } + + return outputs[bestIndex]; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode5Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode5Encoder.cs new file mode 100644 index 0000000..1ea71f2 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode5Encoder.cs @@ -0,0 +1,111 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode5Encoder + { + + private static ReadOnlySpan PartitionTable => new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + const int Subset = 0; + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation) + { + var type = Bc7BlockType.Type5; + + Span outputs = stackalloc Bc7Block[4]; + + for (var rotation = 0; rotation < 4; rotation++) + { + var rotatedBlock = Bc7EncodingHelpers.RotateBlockColors(block, rotation); + var output = new Bc7Block(); + + Bc7EncodingHelpers.GetInitialUnscaledEndpoints(rotatedBlock, out var ep0, out var ep1); + + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out var _); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out var _); + + byte pBit = 0; //fake pBit + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, rotatedBlock, ref scaledEp0, + ref scaledEp1, ref pBit, ref pBit, startingVariation, PartitionTable, Subset, false, true); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); + var colorIndices = new byte[16]; + var alphaIndices = new byte[16]; + Bc7EncodingHelpers.FillAlphaColorIndices(type, rotatedBlock, + ep0, + ep1, + colorIndices, alphaIndices); + + var needsRedo = false; + + if ((colorIndices[0] & 0b10) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + var alpha0 = scaledEp0.a; + var alpha1 = scaledEp1.a; + + scaledEp0 = scaledEp1; + scaledEp1 = c; + scaledEp0.a = alpha0; + scaledEp1.a = alpha1; + + needsRedo = true; + } + if ((alphaIndices[0] & 0b10) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var a = scaledEp0.a; + + scaledEp0.a = scaledEp1.a; + scaledEp1.a = a; + + needsRedo = true; + } + + if (needsRedo) + { + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, 0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, 0); + Bc7EncodingHelpers.FillAlphaColorIndices(type, rotatedBlock, + ep0, + ep1, + colorIndices, alphaIndices); + } + + + output.PackType5(rotation, new[]{ + new byte[]{scaledEp0.r, scaledEp0.g, scaledEp0.b}, + new byte[]{scaledEp1.r, scaledEp1.g, scaledEp1.b}, + }, + new[] { scaledEp0.a, scaledEp1.a }, + colorIndices, alphaIndices); + + outputs[rotation] = output; + } + + var bestIndex = 0; + float bestError = 0; + var first = true; + + // Find best out of generated blocks + for (var i = 0; i < outputs.Length; i++) + { + var decoded = outputs[i].Decode(); + + var error = block.CalculateYCbCrAlphaError(decoded); + if (error < bestError || first) + { + first = false; + bestError = error; + bestIndex = i; + } + } + + return outputs[bestIndex]; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode6Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode6Encoder.cs new file mode 100644 index 0000000..287656f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode6Encoder.cs @@ -0,0 +1,72 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode6Encoder + { + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation) + { + var hasAlpha = block.HasTransparentPixels(); + + var output = new Bc7Block(); + Bc7EncodingHelpers.GetInitialUnscaledEndpoints(block, out var ep0, out var ep1); + + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, Bc7BlockType.Type6, false, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, Bc7BlockType.Type6, false, out var pBit1); + + ReadOnlySpan partitionTable = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + const int subset = 0; + + //Force 255 alpha if fully opaque + if (!hasAlpha) + { + pBit0 = 1; + pBit1 = 1; + } + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(Bc7BlockType.Type6, block, ref scaledEp0, + ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, hasAlpha, hasAlpha); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(Bc7BlockType.Type6, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(Bc7BlockType.Type6, scaledEp1, pBit1); + var indices = new byte[16]; + Bc7EncodingHelpers.FillSubsetIndices(Bc7BlockType.Type6, block, + ep0, + ep1, + partitionTable, subset, indices); + + + + if ((indices[0] & 0b1000) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + var p = pBit0; + + scaledEp0 = scaledEp1; + pBit0 = pBit1; + scaledEp1 = c; + pBit1 = p; + + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(Bc7BlockType.Type6, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(Bc7BlockType.Type6, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(Bc7BlockType.Type6, block, + ep0, + ep1, + partitionTable, subset, indices); + } + + output.PackType6(new[]{ + new byte[]{scaledEp0.r, scaledEp0.g, scaledEp0.b, scaledEp0.a}, + new byte[]{scaledEp1.r, scaledEp1.g, scaledEp1.b, scaledEp1.a}, + }, + new[] { pBit0, pBit1 }, + indices); + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode7Encoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode7Encoder.cs new file mode 100644 index 0000000..d38c00f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/Bc7Mode7Encoder.cs @@ -0,0 +1,81 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class Bc7Mode7Encoder + { + + public static Bc7Block EncodeBlock(RawBlock4X4Rgba32 block, int startingVariation, int bestPartition) + { + var output = new Bc7Block(); + const Bc7BlockType type = Bc7BlockType.Type7; + + var endpoints = new ColorRgba32[4]; + var pBits = new byte[4]; + ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[bestPartition]; + + var indices = new byte[16]; + + var anchorIndices = new int[] { + 0, + Bc7Block.Subsets2AnchorIndices[bestPartition] + }; + + for (var subset = 0; subset < 2; subset++) + { + + Bc7EncodingHelpers.GetInitialUnscaledEndpointsForSubset(block, out var ep0, out var ep1, + partitionTable, subset); + var scaledEp0 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep0, type, false, out var pBit0); + var scaledEp1 = + Bc7EncodingHelpers.ScaleDownEndpoint(ep1, type, false, out var pBit1); + + Bc7EncodingHelpers.OptimizeSubsetEndpointsWithPBit(type, block, ref scaledEp0, + ref scaledEp1, ref pBit0, ref pBit1, startingVariation, partitionTable, subset, true, true); + + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + + if ((indices[anchorIndices[subset]] & 0b10) > 0) //If anchor index most significant bit is 1, switch endpoints + { + var c = scaledEp0; + var p = pBit0; + + scaledEp0 = scaledEp1; + pBit0 = pBit1; + scaledEp1 = c; + pBit1 = p; + + //redo indices + ep0 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp0, pBit0); + ep1 = Bc7EncodingHelpers.ExpandEndpoint(type, scaledEp1, pBit1); + Bc7EncodingHelpers.FillSubsetIndices(type, block, + ep0, + ep1, + partitionTable, subset, indices); + } + + endpoints[subset * 2] = scaledEp0; + endpoints[subset * 2 + 1] = scaledEp1; + pBits[subset * 2] = pBit0; + pBits[subset * 2 + 1] = pBit1; + } + + output.PackType7(bestPartition, new[]{ + new byte[]{endpoints[0].r, endpoints[0].g, endpoints[0].b, endpoints[0].a}, + new byte[]{endpoints[1].r, endpoints[1].g, endpoints[1].b, endpoints[1].a}, + new byte[]{endpoints[2].r, endpoints[2].g, endpoints[2].b, endpoints[2].a}, + new byte[]{endpoints[3].r, endpoints[3].g, endpoints[3].b, endpoints[3].a} + }, + pBits, + indices); + + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/BptcEncodingHelpers.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/BptcEncodingHelpers.cs new file mode 100644 index 0000000..4e10047 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Bptc/BptcEncodingHelpers.cs @@ -0,0 +1,294 @@ +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc +{ + internal static class BptcEncodingHelpers + { + private static readonly byte[] ColorInterpolationWeights2 = [0, 21, 43, 64]; + private static readonly byte[] ColorInterpolationWeights3 = [0, 9, 18, 27, 37, 46, 55, 64]; + private static readonly byte[] ColorInterpolationWeights4 = [0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64]; + + public static int InterpolateInt(int e0, int e1, int index, int indexPrecision) + { + if (indexPrecision == 0) return e0; + var aWeights2 = ColorInterpolationWeights2; + var aWeights3 = ColorInterpolationWeights3; + var aWeights4 = ColorInterpolationWeights4; + + if (indexPrecision == 2) + return (64 - aWeights2[index]) * e0 + aWeights2[index] * e1 + 32 >> 6; + if (indexPrecision == 3) + return (64 - aWeights3[index]) * e0 + aWeights3[index] * e1 + 32 >> 6; + else // indexprecision == 4 + return (64 - aWeights4[index]) * e0 + aWeights4[index] * e1 + 32 >> 6; + } + + public static byte InterpolateByte(byte e0, byte e1, int index, int indexPrecision) + { + if (indexPrecision == 0) return e0; + var aWeights2 = ColorInterpolationWeights2; + var aWeights3 = ColorInterpolationWeights3; + var aWeights4 = ColorInterpolationWeights4; + + if (indexPrecision == 2) + return (byte)((64 - aWeights2[index]) * e0 + aWeights2[index] * e1 + 32 >> 6); + if (indexPrecision == 3) + return (byte)((64 - aWeights3[index]) * e0 + aWeights3[index] * e1 + 32 >> 6); + else // indexprecision == 4 + return (byte)((64 - aWeights4[index]) * e0 + aWeights4[index] * e1 + 32 >> 6); + } + + + + public static int[] Rank2SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters, bool smallIndex = false) + { + var output = Enumerable.Range(0, smallIndex ? 32 : 64).ToArray(); + + + int CalculatePartitionError(int partitionIndex) + { + var error = 0; + ReadOnlySpan partitionTable = Bc7Block.Subsets2PartitionTable[partitionIndex]; + Span subset0 = stackalloc int[numDistinctClusters]; + Span subset1 = stackalloc int[numDistinctClusters]; + var max0Idx = 0; + var max1Idx = 0; + + //Calculate largest cluster index for each subset + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + var r = reducedIndicesBlock[i]; + subset0[r]++; + var count = subset0[r]; + if (count > subset0[max0Idx]) + { + max0Idx = r; + } + } + else + { + var r = reducedIndicesBlock[i]; + subset1[r]++; + var count = subset1[r]; + if (count > subset1[max1Idx]) + { + max1Idx = r; + } + } + } + + // Calculate error by counting as error everything that does not match the largest cluster + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + if (reducedIndicesBlock[i] != max0Idx) error++; + } + else + { + if (reducedIndicesBlock[i] != max1Idx) error++; + } + } + + return error; + } + + output = [.. output.OrderBy(CalculatePartitionError)]; + + return output; + } + + public static int[] Rank3SubsetPartitions(ClusterIndices4X4 reducedIndicesBlock, int numDistinctClusters) + { + var output = Enumerable.Range(0, 64).ToArray(); + + int CalculatePartitionError(int partitionIndex) + { + var error = 0; + ReadOnlySpan partitionTable = Bc7Block.Subsets3PartitionTable[partitionIndex]; + + Span subset0 = stackalloc int[numDistinctClusters]; + Span subset1 = stackalloc int[numDistinctClusters]; + Span subset2 = stackalloc int[numDistinctClusters]; + var max0Idx = 0; + var max1Idx = 0; + var max2Idx = 0; + + //Calculate largest cluster index for each subset + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + var r = reducedIndicesBlock[i]; + subset0[r]++; + var count = subset0[r]; + if (count > subset0[max0Idx]) + { + max0Idx = r; + } + } + else if (partitionTable[i] == 1) + { + var r = reducedIndicesBlock[i]; + subset1[r]++; + var count = subset1[r]; + if (count > subset1[max1Idx]) + { + max1Idx = r; + } + } + else + { + var r = reducedIndicesBlock[i]; + subset2[r]++; + var count = subset2[r]; + if (count > subset2[max2Idx]) + { + max2Idx = r; + } + } + } + + // Calculate error by counting as error everything that does not match the largest cluster + for (var i = 0; i < 16; i++) + { + if (partitionTable[i] == 0) + { + if (reducedIndicesBlock[i] != max0Idx) error++; + } + else if (partitionTable[i] == 1) + { + if (reducedIndicesBlock[i] != max1Idx) error++; + } + else + { + if (reducedIndicesBlock[i] != max2Idx) error++; + } + } + + return error; + } + + output = [.. output.OrderBy(CalculatePartitionError)]; + + return output; + } + } + + internal struct ClusterIndices4X4 + { +#pragma warning disable 0649 + public int i00, i10, i20, i30; + public int i01, i11, i21, i31; + public int i02, i12, i22, i32; + public int i03, i13, i23, i33; +#pragma warning restore 0649 + + public Span AsSpan => MemoryMarshal.CreateSpan(ref i00, 16); + + public int this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public int this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + public int NumClusters + { + get + { + var t = AsSpan; + Span clusters = stackalloc int[16]; + var distinct = 0; + for (var i = 0; i < 16; i++) + { + var cluster = t[i]; + var found = false; + for (var j = 0; j < distinct; j++) + { + if (clusters[j] == cluster) + { + found = true; + break; + } + } + if (!found) + { + clusters[distinct] = cluster; + ++distinct; + } + } + return distinct; + } + } + + /// + /// Reduces block down to adjacent cluster indices. For example, + /// block that contains clusters 5, 16 and 77 will become a block that contains clusters 0, 1 and 2 + /// + public ClusterIndices4X4 Reduce(out int numClusters) + { + var result = new ClusterIndices4X4(); + numClusters = NumClusters; + Span mapKey = stackalloc int[numClusters]; + var indices = AsSpan; + var outIndices = result.AsSpan; + var next = 0; + for (var i = 0; i < 16; i++) + { + var cluster = indices[i]; + var found = false; + for (var j = 0; j < next; j++) + { + if (mapKey[j] == cluster) + { + found = true; + outIndices[i] = j; + break; + } + } + if (!found) + { + outIndices[i] = next; + mapKey[next] = cluster; + ++next; + } + } + + return result; + } + } + + + internal struct IndexBlock4x4 + { +#pragma warning disable 0649 + public byte i00, i10, i20, i30; + public byte i01, i11, i21, i31; + public byte i02, i12, i22, i32; + public byte i03, i13, i23, i33; +#pragma warning restore 0649 + + public Span AsSpan => MemoryMarshal.CreateSpan(ref i00, 16); + + public byte this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public byte this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/ColorChooser.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/ColorChooser.cs new file mode 100644 index 0000000..c19028d --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/ColorChooser.cs @@ -0,0 +1,186 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal static class ColorChooser + { + + public static int ChooseClosestColor4(ReadOnlySpan colors, ColorRgba32 color, float rWeight, float gWeight, float bWeight, out float error) + { + ReadOnlySpan d = stackalloc float[4] { + MathF.Abs(colors[0].r - color.r) * rWeight + + MathF.Abs(colors[0].g - color.g) * gWeight + + MathF.Abs(colors[0].b - color.b) * bWeight, + MathF.Abs(colors[1].r - color.r) * rWeight + + MathF.Abs(colors[1].g - color.g) * gWeight + + MathF.Abs(colors[1].b - color.b) * bWeight, + MathF.Abs(colors[2].r - color.r) * rWeight + + MathF.Abs(colors[2].g - color.g) * gWeight + + MathF.Abs(colors[2].b - color.b) * bWeight, + MathF.Abs(colors[3].r - color.r) * rWeight + + MathF.Abs(colors[3].g - color.g) * gWeight + + MathF.Abs(colors[3].b - color.b) * bWeight, + }; + + var b0 = d[0] > d[3] ? 1 : 0; + var b1 = d[1] > d[2] ? 1 : 0; + var b2 = d[0] > d[2] ? 1 : 0; + var b3 = d[1] > d[3] ? 1 : 0; + var b4 = d[2] > d[3] ? 1 : 0; + + var x0 = b1 & b2; + var x1 = b0 & b3; + var x2 = b0 & b4; + + var idx = x2 | (x0 | x1) << 1; + error = d[idx]; + return idx; + } + + + public static int ChooseClosestColor4AlphaCutoff(ReadOnlySpan colors, ColorRgba32 color, float rWeight, float gWeight, float bWeight, int alphaCutoff, bool hasAlpha, out float error) + { + + if (hasAlpha && color.a < alphaCutoff) + { + error = 0; + return 3; + } + + ReadOnlySpan d = stackalloc float[4] { + MathF.Abs(colors[0].r - color.r) * rWeight + + MathF.Abs(colors[0].g - color.g) * gWeight + + MathF.Abs(colors[0].b - color.b) * bWeight, + MathF.Abs(colors[1].r - color.r) * rWeight + + MathF.Abs(colors[1].g - color.g) * gWeight + + MathF.Abs(colors[1].b - color.b) * bWeight, + MathF.Abs(colors[2].r - color.r) * rWeight + + MathF.Abs(colors[2].g - color.g) * gWeight + + MathF.Abs(colors[2].b - color.b) * bWeight, + + hasAlpha ? 999 : + MathF.Abs(colors[3].r - color.r) * rWeight + + MathF.Abs(colors[3].g - color.g) * gWeight + + MathF.Abs(colors[3].b - color.b) * bWeight, + }; + + var b0 = d[0] > d[2] ? 1 : 0; + var b1 = d[1] > d[3] ? 1 : 0; + var b2 = d[0] > d[3] ? 1 : 0; + var b3 = d[1] > d[2] ? 1 : 0; + var nb3 = d[1] > d[2] ? 0 : 1; + var b4 = d[0] > d[1] ? 1 : 0; + var b5 = d[2] > d[3] ? 1 : 0; + + var idx = nb3 & b4 | b2 & b5 | (b0 & b3 | b1 & b2) << 1; + + error = d[idx]; + return idx; + } + + public static int ChooseClosestColor(Span colors, ColorRgba32 color) + { + var closest = 0; + var closestError = + Math.Abs(colors[0].r - color.r) + + Math.Abs(colors[0].g - color.g) + + Math.Abs(colors[0].b - color.b); + + for (var i = 1; i < colors.Length; i++) + { + var error = + Math.Abs(colors[i].r - color.r) + + Math.Abs(colors[i].g - color.g) + + Math.Abs(colors[i].b - color.b); + if (error < closestError) + { + closest = i; + closestError = error; + } + } + return closest; + } + + public static int ChooseClosestColor(Span colors, ColorRgba32 color) + { + var closest = 0; + var closestError = + Math.Abs(colors[0].r - color.r) + + Math.Abs(colors[0].g - color.g) + + Math.Abs(colors[0].b - color.b) + + Math.Abs(colors[0].a - color.a); + + for (var i = 1; i < colors.Length; i++) + { + var error = + Math.Abs(colors[i].r - color.r) + + Math.Abs(colors[i].g - color.g) + + Math.Abs(colors[i].b - color.b) + + Math.Abs(colors[i].a - color.a); + if (error < closestError) + { + closest = i; + closestError = error; + } + } + return closest; + } + + public static int ChooseClosestColorAlphaCutOff(Span colors, ColorRgba32 color, byte alphaCutOff = 255 / 2) + { + if (color.a <= alphaCutOff) + { + return 3; + } + + var closest = 0; + var closestError = + Math.Abs(colors[0].r - color.r) + + Math.Abs(colors[0].g - color.g) + + Math.Abs(colors[0].b - color.b); + + for (var i = 1; i < colors.Length; i++) + { + if (i == 3) continue; // Skip transparent + var error = + Math.Abs(colors[i].r - color.r) + + Math.Abs(colors[i].g - color.g) + + Math.Abs(colors[i].b - color.b); + if (error < closestError) + { + closest = i; + closestError = error; + } + } + return closest; + } + + public static int ChooseClosestColor(Span colors, ColorYCbCr color, float luminanceMultiplier = 4) + { + var closest = 0; + float closestError = 0; + var first = true; + + for (var i = 0; i < colors.Length; i++) + { + var error = MathF.Abs(colors[i].y - color.y) * luminanceMultiplier + + MathF.Abs(colors[i].cb - color.cb) + + MathF.Abs(colors[i].cr - color.cr); + if (first) + { + closestError = error; + first = false; + } + else if (error < closestError) + { + closest = i; + closestError = error; + } + } + return closest; + } + + public static int ChooseClosestColor(Span colors, ColorRgba32 color, float luminanceMultiplier = 4) + => ChooseClosestColor(colors, new ColorYCbCr(color), luminanceMultiplier); + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/ColorVariationGenerator.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/ColorVariationGenerator.cs new file mode 100644 index 0000000..d0e650d --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/ColorVariationGenerator.cs @@ -0,0 +1,51 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal static class ColorVariationGenerator + { + + private static readonly int[] variatePatternEp0R = [1, 1, 0, 0, -1, 0, 0, -1, 1, -1, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + private static readonly int[] variatePatternEp0G = [1, 0, 1, 0, 0, -1, 0, -1, 1, -1, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + private static readonly int[] variatePatternEp0B = [1, 0, 0, 1, 0, 0, -1, -1, 1, -1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0]; + private static readonly int[] variatePatternEp1R = [-1, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0]; + private static readonly int[] variatePatternEp1G = [-1, 0, -1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 1, 0, 0, -1, 0]; + private static readonly int[] variatePatternEp1B = [-1, 0, 0, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 1, 0, 0, -1]; + public static int VarPatternCount => variatePatternEp0R.Length; + + public static (ColorRgb565, ColorRgb565) Variate565(ColorRgb565 c0, ColorRgb565 c1, int i) + { + var idx = i % variatePatternEp0R.Length; + var newEp0 = new ColorRgb565(); + var newEp1 = new ColorRgb565(); + + newEp0.RawR = ByteHelper.ClampToByte(c0.RawR + variatePatternEp0R[idx]); + newEp0.RawG = ByteHelper.ClampToByte(c0.RawG + variatePatternEp0G[idx]); + newEp0.RawB = ByteHelper.ClampToByte(c0.RawB + variatePatternEp0B[idx]); + + newEp1.RawR = ByteHelper.ClampToByte(c1.RawR + variatePatternEp1R[idx]); + newEp1.RawG = ByteHelper.ClampToByte(c1.RawG + variatePatternEp1G[idx]); + newEp1.RawB = ByteHelper.ClampToByte(c1.RawB + variatePatternEp1B[idx]); + + return (newEp0, newEp1); + } + + + public static ((int, int, int), (int, int, int)) VariateInt((int, int, int) ep0, + (int, int, int) ep1, int i) + { + var idx = i % variatePatternEp0R.Length; + + return (( + ep0.Item1 + variatePatternEp0R[idx], + ep0.Item2 + variatePatternEp0G[idx], + ep0.Item3 + variatePatternEp0B[idx] + ), + ( + ep1.Item1 + variatePatternEp1R[idx], + ep1.Item2 + variatePatternEp1G[idx], + ep1.Item3 + variatePatternEp1B[idx] + )); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/CompressionQuality.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/CompressionQuality.cs new file mode 100644 index 0000000..47cb573 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/CompressionQuality.cs @@ -0,0 +1,18 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + public enum CompressionQuality + { + /// + /// Fast, but low Quality. Especially bad with gradients. + /// + Fast, + /// + /// Strikes a balance between speed and Quality. Good enough for most purposes. + /// + Balanced, + /// + /// Aims for best Quality encoding. Can be very slow. + /// + BestQuality + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/IBcBlockEncoder.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/IBcBlockEncoder.cs new file mode 100644 index 0000000..83a8cf8 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/IBcBlockEncoder.cs @@ -0,0 +1,15 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal interface IBcBlockEncoder where T : unmanaged + { + byte[] Encode(T[] blocks, int blockWidth, int blockHeight, CompressionQuality quality, OperationContext context); + void EncodeBlock(T block, CompressionQuality quality, Span output); + GlInternalFormat GetInternalFormat(); + GlFormat GetBaseInternalFormat(); + DxgiFormat GetDxgiFormat(); + int GetBlockSize(); + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/LeastSquares.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/LeastSquares.cs new file mode 100644 index 0000000..842cd3e --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/LeastSquares.cs @@ -0,0 +1,159 @@ +using System.Numerics; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using Half = TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.Half; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + /// + /// Least squares optimization from https://github.com/knarkowicz/GPURealTimeBC6H + /// Code is public domain + /// + internal static class LeastSquares + { + + private static int ComputeIndex4(float texelPos, float endPoint0Pos, float endPoint1Pos) + { + var r = (texelPos - endPoint0Pos) / (endPoint1Pos - endPoint0Pos); + return (int)Math.Clamp(r * 15f /*14.93333f + 0.03333f + 0.5f*/, 0.0f, 15.0f); + } + + private static int ComputeIndex3(float texelPos, float endPoint0Pos, float endPoint1Pos) + { + var r = (texelPos - endPoint0Pos) / (endPoint1Pos - endPoint0Pos); + return (int)Math.Clamp(r * 6.98182f + 0.00909f + 0.5f, 0.0f, 7.0f); + } + + private static uint F32ToF16(float f32) + { + return Half.GetBits(new Half(f32)); + } + + private static Vector3 F32ToF16(Vector3 f32) + { + return new Vector3( + F32ToF16(f32.X), + F32ToF16(f32.Y), + F32ToF16(f32.Z) + ); + } + + private static float F16ToF32(uint f16) + { + return Half.ToHalf((ushort)f16); + } + + private static Vector3 F16ToF32(Vector3 f16) + { + return new Vector3( + F16ToF32((uint)f16.X), + F16ToF32((uint)f16.Y), + F16ToF32((uint)f16.Z) + ); + } + + public static void OptimizeEndpoints1Sub(RawBlock4X4RgbFloat block, ref ColorRgbFloat ep0, ref ColorRgbFloat ep1) + { + var ep0v = ep0.ToVector3(); + var ep1v = ep1.ToVector3(); + + var pixels = block.AsSpan; + + var blockDir = ep1v - ep0v; + blockDir /= blockDir.X + blockDir.Y + blockDir.Z; + + var endPoint0Pos = (float)F32ToF16(Vector3.Dot(ep0v, blockDir)); + var endPoint1Pos = (float)F32ToF16(Vector3.Dot(ep1v, blockDir)); + + var alphaTexelSum = new Vector3(); + var betaTexelSum = new Vector3(); + var alphaBetaSum = 0.0f; + var alphaSqSum = 0.0f; + var betaSqSum = 0.0f; + + for (var i = 0; i < 16; i++) + { + var texelPos = (float)F32ToF16(Vector3.Dot(pixels[i].ToVector3(), blockDir)); + var texelIndex = ComputeIndex4(texelPos, endPoint0Pos, endPoint1Pos); + + var beta = Math.Clamp(texelIndex / 15.0f, 0f, 1f); + var alpha = 1.0f - beta; + + var texelF16 = F32ToF16(pixels[i].ToVector3()); + alphaTexelSum += alpha * texelF16; + betaTexelSum += beta * texelF16; + + alphaBetaSum += alpha * beta; + + alphaSqSum += alpha * alpha; + betaSqSum += beta * beta; + } + + var det = alphaSqSum * betaSqSum - alphaBetaSum * alphaBetaSum; + + if (MathF.Abs(det) > 0.00001f) + { + var detRcp = 1f / det; + var ep0f16 = detRcp * (alphaTexelSum * betaSqSum - betaTexelSum * alphaBetaSum); + var ep1f16 = detRcp * (betaTexelSum * alphaSqSum - alphaTexelSum * alphaBetaSum); + ep0f16 = Vector3.Clamp(ep0f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep1f16 = Vector3.Clamp(ep1f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep0 = new ColorRgbFloat(F16ToF32(ep0f16)); + ep1 = new ColorRgbFloat(F16ToF32(ep1f16)); + } + } + + public static void OptimizeEndpoints2Sub(RawBlock4X4RgbFloat block, ref ColorRgbFloat ep0, ref ColorRgbFloat ep1, int partitionSetId, int subsetIndex) + { + var ep0v = ep0.ToVector3(); + var ep1v = ep1.ToVector3(); + + var pixels = block.AsSpan; + + var blockDir = ep1v - ep0v; + blockDir /= blockDir.X + blockDir.Y + blockDir.Z; + + var endPoint0Pos = (float)F32ToF16(Vector3.Dot(ep0v, blockDir)); + var endPoint1Pos = (float)F32ToF16(Vector3.Dot(ep1v, blockDir)); + + var alphaTexelSum = new Vector3(); + var betaTexelSum = new Vector3(); + var alphaBetaSum = 0.0f; + var alphaSqSum = 0.0f; + var betaSqSum = 0.0f; + + for (var i = 0; i < 16; i++) + { + if (Bc6Block.Subsets2PartitionTable[partitionSetId][i] == subsetIndex) + { + var texelPos = (float)F32ToF16(Vector3.Dot(pixels[i].ToVector3(), blockDir)); + var texelIndex = ComputeIndex3(texelPos, endPoint0Pos, endPoint1Pos); + + var beta = Math.Clamp(texelIndex / 7.0f, 0f, 1f); + var alpha = 1.0f - beta; + + var texelF16 = F32ToF16(pixels[i].ToVector3()); + alphaTexelSum += alpha * texelF16; + betaTexelSum += beta * texelF16; + + alphaBetaSum += alpha * beta; + + alphaSqSum += alpha * alpha; + betaSqSum += beta * beta; + } + } + + var det = alphaSqSum * betaSqSum - alphaBetaSum * alphaBetaSum; + + if (MathF.Abs(det) > 0.00001f) + { + var detRcp = 1f / det; + var ep0f16 = detRcp * (alphaTexelSum * betaSqSum - betaTexelSum * alphaBetaSum); + var ep1f16 = detRcp * (betaTexelSum * alphaSqSum - alphaTexelSum * alphaBetaSum); + ep0f16 = Vector3.Clamp(ep0f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep1f16 = Vector3.Clamp(ep1f16, Vector3.Zero, new Vector3(Half.MaxValue.Value)); + ep0 = new ColorRgbFloat(F16ToF32(ep0f16)); + ep1 = new ColorRgbFloat(F16ToF32(ep1f16)); + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderInputOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderInputOptions.cs new file mode 100644 index 0000000..21f9abf --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderInputOptions.cs @@ -0,0 +1,31 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Options +{ + /// + /// The input options for the decoder. + /// + public class EncoderInputOptions + { + /// + /// If true, when encoding to R8 raw format, + /// use the pixel luminance instead of just the red channel. Default is false. (Does not apply to BC4 format) + /// + public bool LuminanceAsRed { get; set; } = false; + + /// + /// The color channel to take for the values of a BC4 block. Default is red. + /// + public ColorComponent Bc4Component { get; set; } = ColorComponent.R; + + /// + /// The color channel to take for the values of the first BC5 block. Default is red. + /// + public ColorComponent Bc5Component1 { get; set; } = ColorComponent.R; + + /// + /// The color channel to take for the values of the second BC5 block. Default is green. + /// + public ColorComponent Bc5Component2 { get; set; } = ColorComponent.G; + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderOptions.cs new file mode 100644 index 0000000..325f14f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderOptions.cs @@ -0,0 +1,28 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Options +{ + /// + /// General options for the encoder. + /// + public class EncoderOptions + { + /// + /// Whether the blocks should be encoded in parallel. This can be much faster than single-threaded encoding, + /// but is slow if multiple textures are being processed at the same time. + /// When a debugger is attached, the encoder defaults to single-threaded operation to ease debugging. + /// Default is true. + /// + public bool IsParallel { get; set; } = true; + + /// + /// Determines how many tasks should be used for parallel processing. + /// + public int TaskCount { get; set; } = Environment.ProcessorCount; + + /// + /// The progress context for the operation. + /// + public IProgress Progress { get; set; } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderOutputOptions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderOutputOptions.cs new file mode 100644 index 0000000..f1daf8f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/Options/EncoderOutputOptions.cs @@ -0,0 +1,55 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Options +{ + /// + /// The output options for the encoder. + /// + public class EncoderOutputOptions + { + /// + /// Whether to generate mipMaps. Default is true. + /// + public bool GenerateMipMaps { get; set; } = true; + + /// + /// The maximum number of mipmap levels to generate. -1 or 0 is unbounded. + /// Default is -1. + /// + public int MaxMipMapLevel { get; set; } = -1; + + /// + /// The compression Format to use. Default is Bc1. + /// + public CompressionFormat Format { get; set; } = CompressionFormat.Bc1; + + /// + /// The Quality of the compression. Use either fast or balanced for testing. + /// Fast can be used for near real-time encoding for most algorithms. + /// Use bestQuality when needed. Default is balanced. + /// + public CompressionQuality Quality { get; set; } = CompressionQuality.Balanced; + + /// + /// The output file Format of the data. Either Ktx or Dds. + /// Default is Ktx. + /// + public OutputFileFormat FileFormat { get; set; } = OutputFileFormat.Ktx; + + /// + /// The DDS file Format doesn't seem to have a standard for indicating whether a BC1 texture + /// includes 1bit of alpha. This option will write DDPF_ALPHAPIXELS flag to the header + /// to indicate the presence of an alpha channel. Some programs read and write this flag, + /// but some programs don't like it and get confused. Your mileage may vary. + /// Default is false. + /// + public bool DdsBc1WriteAlphaFlag { get; set; } = false; + + /// + /// When writing a dds file, always prefer using the Dxt10 header + /// to write the compression format instead of DwFourCC. + /// + public bool DdsPreferDxt10Header { get; set; } = false; + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/RawEncoders.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/RawEncoders.cs new file mode 100644 index 0000000..a309a53 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Encoder/RawEncoders.cs @@ -0,0 +1,258 @@ +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder +{ + internal interface IRawEncoder + { + byte[] Encode(ReadOnlyMemory pixels); + GlInternalFormat GetInternalFormat(); + GlFormat GetBaseInternalFormat(); + GlFormat GetGlFormat(); + GlType GetGlType(); + uint GetGlTypeSize(); + DxgiFormat GetDxgiFormat(); + } + + internal class RawLuminanceEncoder(bool useLuminance) : IRawEncoder + { + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + if (useLuminance) + { + output[i] = (byte)(new ColorYCbCr(span[i]).y * 255); + } + else + { + output[i] = span[i].r; + } + + } + return output; + } + + public GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlR8; + } + + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRed; + } + + public GlFormat GetGlFormat() + { + return GlFormat.GlRed; + } + + public GlType GetGlType() + { + return GlType.GlByte; + } + + public uint GetGlTypeSize() + { + return 1; + } + + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatR8Unorm; + } + } + + internal class RawRgEncoder : IRawEncoder + { + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 2]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 2] = span[i].r; + output[i * 2 + 1] = span[i].g; + } + return output; + } + + public GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlRg8; + } + + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRg; + } + + public GlFormat GetGlFormat() + { + return GlFormat.GlRg; + } + + public GlType GetGlType() + { + return GlType.GlByte; + } + + public uint GetGlTypeSize() + { + return 1; + } + + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatR8G8Unorm; + } + } + + internal class RawRgbEncoder : IRawEncoder + { + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 3]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 3] = span[i].r; + output[i * 3 + 1] = span[i].g; + output[i * 3 + 2] = span[i].b; + } + return output; + } + + public GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlRgb8; + } + + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgb; + } + + public GlFormat GetGlFormat() + { + return GlFormat.GlRgb; + } + + public GlType GetGlType() + { + return GlType.GlByte; + } + + public uint GetGlTypeSize() + { + return 1; + } + + public DxgiFormat GetDxgiFormat() + { + throw new NotSupportedException("RGB Format is not supported for dds files."); + } + } + + internal class RawRgbaEncoder : IRawEncoder + { + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 4]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 4] = span[i].r; + output[i * 4 + 1] = span[i].g; + output[i * 4 + 2] = span[i].b; + output[i * 4 + 3] = span[i].a; + } + return output; + } + + public GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlRgba8; + } + + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlRgba; + } + + public GlFormat GetGlFormat() + { + return GlFormat.GlRgba; + } + + public GlType GetGlType() + { + return GlType.GlByte; + } + + public uint GetGlTypeSize() + { + return 1; + } + + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatR8G8B8A8Unorm; + } + } + + internal class RawBgraEncoder : IRawEncoder + { + public byte[] Encode(ReadOnlyMemory pixels) + { + var span = pixels.Span; + + var output = new byte[pixels.Length * 4]; + for (var i = 0; i < pixels.Length; i++) + { + output[i * 4] = span[i].b; + output[i * 4 + 1] = span[i].g; + output[i * 4 + 2] = span[i].r; + output[i * 4 + 3] = span[i].a; + } + return output; + } + + public GlInternalFormat GetInternalFormat() + { + return GlInternalFormat.GlBgra8Extension; + } + + public GlFormat GetBaseInternalFormat() + { + return GlFormat.GlBgra; + } + + public GlFormat GetGlFormat() + { + return GlFormat.GlBgra; + } + + public GlType GetGlType() + { + return GlType.GlByte; + } + + public uint GetGlTypeSize() + { + return 1; + } + + public DxgiFormat GetDxgiFormat() + { + return DxgiFormat.DxgiFormatB8G8R8A8Unorm; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Bc6Block.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Bc6Block.cs new file mode 100644 index 0000000..09e0019 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Bc6Block.cs @@ -0,0 +1,1404 @@ +using System.Diagnostics; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + + internal enum Bc6BlockType : uint + { + Type0 = 0, // Mode 1 + Type1 = 1, // Mode 2 + Type2 = 2, // Mode 3 + Type6 = 6, // Mode 4 + Type10 = 10, // Mode 5 + Type14 = 14, // Mode 6 + Type18 = 18, // Mode 7 + Type22 = 22, // Mode 8 + Type26 = 26, // Mode 9 + Type30 = 30, // Mode 10 + Type3 = 3, // Mode 11 + Type7 = 7, // Mode 12 + Type11 = 11, // Mode 13 + Type15 = 15, // Mode 14 + Unknown + } + + internal static class Bc6BlockTypeExtensions + { + public static bool HasSubsets(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type3 => false, + Bc6BlockType.Type7 => false, + Bc6BlockType.Type11 => false, + Bc6BlockType.Type15 => false, + _ => true + }; + + public static bool HasTransformedEndpoints(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type3 => false, + Bc6BlockType.Type30 => false, + _ => true + }; + + public static int EndpointBits(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type0 => 10, + Bc6BlockType.Type1 => 7, + Bc6BlockType.Type2 => 11, + Bc6BlockType.Type6 => 11, + Bc6BlockType.Type10 => 11, + Bc6BlockType.Type14 => 9, + Bc6BlockType.Type18 => 8, + Bc6BlockType.Type22 => 8, + Bc6BlockType.Type26 => 8, + Bc6BlockType.Type30 => 6, + Bc6BlockType.Type3 => 10, + Bc6BlockType.Type7 => 11, + Bc6BlockType.Type11 => 12, + Bc6BlockType.Type15 => 16, + _ => 0 + }; + + public static (int, int, int) DeltaBits(this Bc6BlockType Type) => Type switch + { + Bc6BlockType.Type0 => (5, 5, 5), + Bc6BlockType.Type1 => (6, 6, 6), + Bc6BlockType.Type2 => (5, 4, 4), + Bc6BlockType.Type6 => (4, 5, 4), + Bc6BlockType.Type10 => (4, 4, 5), + Bc6BlockType.Type14 => (5, 5, 5), + Bc6BlockType.Type18 => (6, 5, 5), + Bc6BlockType.Type22 => (5, 6, 5), + Bc6BlockType.Type26 => (5, 5, 6), + Bc6BlockType.Type30 => (0, 0, 0), + Bc6BlockType.Type3 => (0, 0, 0), + Bc6BlockType.Type7 => (9, 9, 9), + Bc6BlockType.Type11 => (8, 8, 8), + Bc6BlockType.Type15 => (4, 4, 4), + _ => (0, 0, 0) + }; + } + + internal struct Bc6Block + { + public ulong lowBits; + public ulong highBits; + + public static readonly int[][] Subsets2PartitionTable = new int[32][]{ + new[] {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, + new[] {0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}, + new[] {0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + new[] {0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1}, + new[] {0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0}, + new[] {0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0}, + new[] {0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0}, + new[] {0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0} + }; + + public static readonly int[] Subsets2AnchorIndices = { + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 2, 8, 2, 2, 8, 8, 15, + 2, 8, 2, 2, 8, 8, 2, 2, + 15, 15, 6, 8, 2, 8, 15, 15, + 2, 8, 2, 2, 2, 15, 15, 6, + 6, 2, 6, 8, 15, 15, 2, 2, + 15, 15, 15, 15, 15, 2, 2, 15 + }; + + public static readonly RawBlock4X4RgbFloat ErrorBlock = new RawBlock4X4RgbFloat(new ColorRgbFloat(1, 0, 1)); + + public static readonly Bc6BlockType[] Subsets1Types = + { + Bc6BlockType.Type3, + Bc6BlockType.Type7, + Bc6BlockType.Type11, + Bc6BlockType.Type15 + }; + + public static readonly Bc6BlockType[] Subsets2Types = + { + Bc6BlockType.Type0 , + Bc6BlockType.Type1 , + Bc6BlockType.Type2 , + Bc6BlockType.Type6 , + Bc6BlockType.Type10, + Bc6BlockType.Type14, + Bc6BlockType.Type18, + Bc6BlockType.Type22, + Bc6BlockType.Type26, + Bc6BlockType.Type30 + }; + + public readonly Bc6BlockType Type + { + get + { + const ulong smallMask = 0b11; + const ulong bigMask = 0b11111; + // Type 0 or 1 + if ((lowBits & smallMask) < 2) + { + return (Bc6BlockType)(lowBits & smallMask); + } + else + { + var typeNum = lowBits & bigMask; + switch (typeNum) + { + case 2: return Bc6BlockType.Type2; + case 3: return Bc6BlockType.Type3; + case 6: return Bc6BlockType.Type6; + case 7: return Bc6BlockType.Type7; + case 10: return Bc6BlockType.Type10; + case 11: return Bc6BlockType.Type11; + case 14: return Bc6BlockType.Type14; + case 15: return Bc6BlockType.Type15; + case 18: return Bc6BlockType.Type18; + case 22: return Bc6BlockType.Type22; + case 26: return Bc6BlockType.Type26; + case 30: return Bc6BlockType.Type30; + default: return Bc6BlockType.Unknown; + } + } + } + } + + public readonly bool HasSubsets => Type.HasSubsets(); + + public readonly int NumEndpoints => HasSubsets ? 4 : 2; + + public readonly bool HasTransformedEndpoints => Type.HasTransformedEndpoints(); + + public readonly int PartitionSetId => HasSubsets ? ByteHelper.Extract5(highBits, 13) : -1; + + public readonly int EndpointBits => Type.EndpointBits(); + + public readonly (int, int, int) DeltaBits => Type.DeltaBits(); + + public readonly int ColorIndexBitCount => HasSubsets ? 3 : 4; + + internal void StoreEp0((int, int, int) endpoint) + { + var r0 = (ulong)endpoint.Item1; + var g0 = (ulong)endpoint.Item2; + var b0 = (ulong)endpoint.Item3; + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 5, Math.Min(10, EndpointBits), r0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 15, Math.Min(10, EndpointBits), g0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 25, Math.Min(10, EndpointBits), b0); + + switch (Type) + { + case Bc6BlockType.Type2: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 49, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 59, 1, b0 >> 10); + + break; + case Bc6BlockType.Type6: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 39, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 59, 1, b0 >> 10); + + break; + case Bc6BlockType.Type10: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 39, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 49, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b0 >> 10); + + break; + case Bc6BlockType.Type7: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 44, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 54, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 64, 1, b0 >> 10); + + break; + case Bc6BlockType.Type11: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 44, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 54, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 64, 1, b0 >> 10); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 43, 1, r0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 53, 1, g0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 63, 1, b0 >> 11); + + break; + case Bc6BlockType.Type15: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 44, 1, r0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 54, 1, g0 >> 10); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 64, 1, b0 >> 10); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 43, 1, r0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 53, 1, g0 >> 11); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 63, 1, b0 >> 11); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 42, 1, r0 >> 12); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 52, 1, g0 >> 12); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 62, 1, b0 >> 12); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 41, 1, r0 >> 13); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 51, 1, g0 >> 13); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 61, 1, b0 >> 13); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r0 >> 14); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g0 >> 14); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b0 >> 14); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 39, 1, r0 >> 15); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 49, 1, g0 >> 15); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 59, 1, b0 >> 15); + + break; + } + } + + internal readonly (int, int, int) ExtractEp0() + { + ulong r0 = 0; + ulong g0 = 0; + ulong b0 = 0; + + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, Math.Min(10, EndpointBits)); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, Math.Min(10, EndpointBits)); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, Math.Min(10, EndpointBits)); + + switch (Type) + { + case Bc6BlockType.Type2: + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 49, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 59, 1) << 10; + break; + case Bc6BlockType.Type6: + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 39, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 59, 1) << 10; + break; + case Bc6BlockType.Type10: + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 39, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 49, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 10; + break; + case Bc6BlockType.Type7: + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, 10); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, 10); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, 10); + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 44, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 54, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 64, 1) << 10; + break; + case Bc6BlockType.Type11: + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, 10); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, 10); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, 10); + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 44, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 54, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 64, 1) << 10; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 43, 1) << 11; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 53, 1) << 11; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 63, 1) << 11; + break; + case Bc6BlockType.Type15: + r0 = ByteHelper.ExtractFrom128(lowBits, highBits, 5, 10); + g0 = ByteHelper.ExtractFrom128(lowBits, highBits, 15, 10); + b0 = ByteHelper.ExtractFrom128(lowBits, highBits, 25, 10); + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 44, 1) << 10; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 54, 1) << 10; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 64, 1) << 10; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 43, 1) << 11; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 53, 1) << 11; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 63, 1) << 11; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 42, 1) << 12; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 52, 1) << 12; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 62, 1) << 12; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 41, 1) << 13; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 51, 1) << 13; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 61, 1) << 13; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 14; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 14; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 14; + + r0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 39, 1) << 15; + g0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 49, 1) << 15; + b0 |= ByteHelper.ExtractFrom128(lowBits, highBits, 59, 1) << 15; + break; + } + + return ((int)r0, (int)g0, (int)b0); + } + + internal void StoreEp1((int, int, int) endpoint) + { + var r1 = (ulong)endpoint.Item1; + var g1 = (ulong)endpoint.Item2; + var b1 = (ulong)endpoint.Item3; + + if (HasTransformedEndpoints) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 35, Math.Min(5, DeltaBits.Item1), r1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 45, Math.Min(5, DeltaBits.Item2), g1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 55, Math.Min(5, DeltaBits.Item3), b1); + + } + + switch (Type) + { + case Bc6BlockType.Type1: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b1 >> 5); + + + break; + case Bc6BlockType.Type18: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, r1 >> 5); + + + break; + case Bc6BlockType.Type22: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, g1 >> 5); + + break; + case Bc6BlockType.Type26: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b1 >> 5); + + break; + case Bc6BlockType.Type30: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 35, 6, r1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 45, 6, g1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 55, 6, b1); + + break; + case Bc6BlockType.Type3: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 35, 10, r1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 45, 10, g1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 55, 10, b1); + + break; + case Bc6BlockType.Type7: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 4, r1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 4, g1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 4, b1 >> 5); + + break; + case Bc6BlockType.Type11: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 3, r1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 3, g1 >> 5); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 3, b1 >> 5); + + break; + } + } + + internal readonly (int, int, int) ExtractEp1() + { + ulong r1 = 0; + ulong g1 = 0; + ulong b1 = 0; + + if (HasTransformedEndpoints) + { + r1 = ByteHelper.ExtractFrom128(lowBits, highBits, 35, Math.Min(5, DeltaBits.Item1)); + g1 = ByteHelper.ExtractFrom128(lowBits, highBits, 45, Math.Min(5, DeltaBits.Item2)); + b1 = ByteHelper.ExtractFrom128(lowBits, highBits, 55, Math.Min(5, DeltaBits.Item3)); + } + + switch (Type) + { + case Bc6BlockType.Type1: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 5; + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 5; + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 5; + + break; + case Bc6BlockType.Type18: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 5; + + break; + case Bc6BlockType.Type22: + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1) << 5; + + break; + case Bc6BlockType.Type26: + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 5; + + break; + case Bc6BlockType.Type30: + r1 = ByteHelper.ExtractFrom128(lowBits, highBits, 35, 6); + g1 = ByteHelper.ExtractFrom128(lowBits, highBits, 45, 6); + b1 = ByteHelper.ExtractFrom128(lowBits, highBits, 55, 6); + + break; + case Bc6BlockType.Type3: + r1 = ByteHelper.ExtractFrom128(lowBits, highBits, 35, 10); + g1 = ByteHelper.ExtractFrom128(lowBits, highBits, 45, 10); + b1 = ByteHelper.ExtractFrom128(lowBits, highBits, 55, 10); + + break; + case Bc6BlockType.Type7: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 4) << 5; + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 4) << 5; + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 4) << 5; + + break; + case Bc6BlockType.Type11: + r1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 3) << 5; + g1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 50, 3) << 5; + b1 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 3) << 5; + + break; + } + + return ((int)r1, (int)g1, (int)b1); + } + + internal void StoreEp2((int, int, int) endpoint) + { + var r2 = (ulong)endpoint.Item1; + var g2 = (ulong)endpoint.Item2; + var b2 = (ulong)endpoint.Item3; + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 65, Math.Min(5, DeltaBits.Item1), r2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 41, 4, g2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 61, 4, b2); + + switch (Type) + { + case Bc6BlockType.Type0: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 2, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 3, 1, b2 >> 4); + + break; + case Bc6BlockType.Type1: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, r2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 2, 1, g2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 22, 1, b2 >> 5); + + break; + case Bc6BlockType.Type6: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 75, 1, g2 >> 4); + + break; + case Bc6BlockType.Type10: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, b2 >> 4); + + break; + case Bc6BlockType.Type14: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + + break; + case Bc6BlockType.Type18: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, r2 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + + break; + case Bc6BlockType.Type22: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, g2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + + break; + case Bc6BlockType.Type26: + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b2 >> 5); + + break; + case Bc6BlockType.Type30: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 65, 6, r2); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 24, 1, g2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 21, 1, g2 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 14, 1, b2 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 22, 1, b2 >> 5); + + break; + } + } + + internal readonly (int, int, int) ExtractEp2() + { + ulong r2 = 0; + ulong g2 = 0; + ulong b2 = 0; + + r2 = ByteHelper.ExtractFrom128(lowBits, highBits, 65, Math.Min(5, DeltaBits.Item1)); + g2 = ByteHelper.ExtractFrom128(lowBits, highBits, 41, 4); + b2 = ByteHelper.ExtractFrom128(lowBits, highBits, 61, 4); + + switch (Type) + { + case Bc6BlockType.Type0: + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 2, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 3, 1) << 4; + break; + case Bc6BlockType.Type1: + r2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 5; + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 2, 1) << 5; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 22, 1) << 5; + + break; + case Bc6BlockType.Type6: + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 75, 1) << 4; + + break; + case Bc6BlockType.Type10: + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + break; + case Bc6BlockType.Type14: + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + + break; + case Bc6BlockType.Type18: + r2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 5; + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + + break; + case Bc6BlockType.Type22: + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 5; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + break; + case Bc6BlockType.Type26: + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 5; + break; + case Bc6BlockType.Type30: + + r2 = ByteHelper.ExtractFrom128(lowBits, highBits, 65, 6); + + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 24, 1) << 4; + g2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 21, 1) << 5; + + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 14, 1) << 4; + b2 |= ByteHelper.ExtractFrom128(lowBits, highBits, 22, 1) << 5; + + break; + } + + return ((int)r2, (int)g2, (int)b2); + } + + internal void StoreEp3((int, int, int) endpoint) + { + var r3 = (ulong)endpoint.Item1; + var g3 = (ulong)endpoint.Item2; + var b3 = (ulong)endpoint.Item3; + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 71, Math.Min(5, DeltaBits.Item1), r3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 51, 4, g3); + + switch (Type) + { + case Bc6BlockType.Type0: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 4, 1, b3 >> 4); + + break; + case Bc6BlockType.Type1: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, r3 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 3, 2, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 12, 2, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 32, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 5); + + break; + case Bc6BlockType.Type2: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + + break; + case Bc6BlockType.Type6: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 69, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + + break; + case Bc6BlockType.Type10: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 69, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 75, 1, b3 >> 4); + + break; + case Bc6BlockType.Type14: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + + break; + case Bc6BlockType.Type18: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, r3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 13, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + + break; + case Bc6BlockType.Type22: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, g3 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 13, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 60, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + + break; + case Bc6BlockType.Type26: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 40, 1, g3 >> 4); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 50, 1, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 13, 1, b3 >> 1); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 70, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 76, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 5); + + break; + case Bc6BlockType.Type30: + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 71, 6, r3); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 11, 1, g3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 31, 1, g3 >> 5); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 12, 2, b3 >> 0); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 23, 1, b3 >> 2); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 32, 1, b3 >> 3); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 34, 1, b3 >> 4); + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, 33, 1, b3 >> 5); + + break; + } + } + + internal readonly (int, int, int) ExtractEp3() + { + ulong r3 = 0; + ulong g3 = 0; + ulong b3 = 0; + + r3 = ByteHelper.ExtractFrom128(lowBits, highBits, 71, Math.Min(5, DeltaBits.Item1)); + g3 = ByteHelper.ExtractFrom128(lowBits, highBits, 51, 4); + + switch (Type) + { + case Bc6BlockType.Type0: + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 4, 1) << 4; + break; + case Bc6BlockType.Type1: + r3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 5; + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 3, 2) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 12, 2); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 32, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + break; + case Bc6BlockType.Type2: + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + + break; + case Bc6BlockType.Type6: + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 69, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + + break; + case Bc6BlockType.Type10: + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 69, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 75, 1) << 4; + + break; + case Bc6BlockType.Type14: + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + + break; + case Bc6BlockType.Type18: + r3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 5; + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 13, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + + break; + case Bc6BlockType.Type22: + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 13, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 60, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + + break; + case Bc6BlockType.Type26: + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 40, 1) << 4; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 50, 1); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 13, 1) << 1; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 70, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 76, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + break; + case Bc6BlockType.Type30: + + r3 = ByteHelper.ExtractFrom128(lowBits, highBits, 71, 6); + + + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 11, 1) << 4; + g3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 31, 1) << 5; + + b3 = ByteHelper.ExtractFrom128(lowBits, highBits, 12, 2); + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 23, 1) << 2; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 32, 1) << 3; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 34, 1) << 4; + b3 |= ByteHelper.ExtractFrom128(lowBits, highBits, 33, 1) << 5; + + break; + } + + return ((int)r3, (int)g3, (int)b3); + } + + private readonly (int, int, int)[] ExtractRawEndpoints(bool signedBc6) + { + var outEndpoints = new (int, int, int)[HasSubsets ? 4 : 2]; + var endpointBits = EndpointBits; + + var (r0, g0, b0) = ExtractEp0(); + + // If bc6h is in signed mode, sign extend the base endpoints + if (signedBc6) + { + r0 = IntHelper.SignExtend(r0, endpointBits); + g0 = IntHelper.SignExtend(g0, endpointBits); + b0 = IntHelper.SignExtend(b0, endpointBits); + } + + outEndpoints[0] = (r0, g0, b0); + + var (r1, g1, b1) = ExtractEp1(); + + if (HasTransformedEndpoints) + { + r1 = IntHelper.SignExtend(r1, DeltaBits.Item1); + g1 = IntHelper.SignExtend(g1, DeltaBits.Item2); + b1 = IntHelper.SignExtend(b1, DeltaBits.Item3); + + r1 = r1 + r0 & (1 << endpointBits) - 1; + g1 = g1 + g0 & (1 << endpointBits) - 1; + b1 = b1 + b0 & (1 << endpointBits) - 1; + } + if (signedBc6) + { + r1 = IntHelper.SignExtend(r1, endpointBits); + g1 = IntHelper.SignExtend(g1, endpointBits); + b1 = IntHelper.SignExtend(b1, endpointBits); + } + + outEndpoints[1] = (r1, g1, b1); + + if (HasSubsets) + { + var (r2, g2, b2) = ExtractEp2(); + var (r3, g3, b3) = ExtractEp3(); + + if (HasTransformedEndpoints) + { + r2 = IntHelper.SignExtend(r2, DeltaBits.Item1); + g2 = IntHelper.SignExtend(g2, DeltaBits.Item2); + b2 = IntHelper.SignExtend(b2, DeltaBits.Item3); + + r2 = r2 + r0 & (1 << endpointBits) - 1; + g2 = g2 + g0 & (1 << endpointBits) - 1; + b2 = b2 + b0 & (1 << endpointBits) - 1; + + r3 = IntHelper.SignExtend(r3, DeltaBits.Item1); + g3 = IntHelper.SignExtend(g3, DeltaBits.Item2); + b3 = IntHelper.SignExtend(b3, DeltaBits.Item3); + + r3 = r3 + r0 & (1 << endpointBits) - 1; + g3 = g3 + g0 & (1 << endpointBits) - 1; + b3 = b3 + b0 & (1 << endpointBits) - 1; + } + + if (signedBc6) + { + r2 = IntHelper.SignExtend(r2, endpointBits); + g2 = IntHelper.SignExtend(g2, endpointBits); + b2 = IntHelper.SignExtend(b2, endpointBits); + + r3 = IntHelper.SignExtend(r3, endpointBits); + g3 = IntHelper.SignExtend(g3, endpointBits); + b3 = IntHelper.SignExtend(b3, endpointBits); + } + + outEndpoints[2] = (r2, g2, b2); + outEndpoints[3] = (r3, g3, b3); + } + + return outEndpoints; + } + + internal static int UnQuantize(int component, int endpointBits, bool signedBc6) + { + int unq; + var sign = false; + + if (!signedBc6) + { + if (endpointBits >= 15) + unq = component; + else if (component == 0) + unq = 0; + else if (component == (1 << endpointBits) - 1) + unq = 0xFFFF; + else + //unq = ((component << 16) + 0x8000) >> endpointBits; + unq = (component << 15) + 0x4000 >> endpointBits - 1; + + } + else + { + if (endpointBits >= 16) + unq = component; + else + { + if (component < 0) + { + sign = true; + component = -component; + } + + if (component == 0) + unq = 0; + else if (component >= (1 << endpointBits - 1) - 1) + unq = 0x7FFF; + else + unq = (component << 15) + 0x4000 >> endpointBits - 1; + + if (sign) + unq = -unq; + } + } + return unq; + } + + internal static (int, int, int) UnQuantize((int, int, int) components, int endpointBits, bool signedBc6) + { + return ( + UnQuantize(components.Item1, endpointBits, signedBc6), + UnQuantize(components.Item2, endpointBits, signedBc6), + UnQuantize(components.Item3, endpointBits, signedBc6) + ); + } + + internal static Half FinishUnQuantize(int component, bool signedBc6) + { + if (!signedBc6) + { + component = component * 31 >> 6; // scale the magnitude by 31/64 + return Half.ToHalf((ushort)component); + } + else // signed format + { + component = component < 0 ? -(-component * 31 >> 5) : component * 31 >> 5; // scale the magnitude by 31/32 + var s = 0; + if (component < 0) + { + s = 0x8000; + component = -component; + } + return Half.ToHalf((ushort)(s | component)); + } + } + + internal static (Half, Half, Half) FinishUnQuantize((int, int, int) components, bool signedBc6) + { + return ( + FinishUnQuantize(components.Item1, signedBc6), + FinishUnQuantize(components.Item2, signedBc6), + FinishUnQuantize(components.Item3, signedBc6) + ); + } + + private static int GetPartitionIndex(int numSubsets, int partitionSetId, int i) + { + switch (numSubsets) + { + case 1: + return 0; + case 2: + return Subsets2PartitionTable[partitionSetId][i]; + default: + throw new ArgumentOutOfRangeException(nameof(numSubsets), numSubsets, "Number of subsets can only be 1, 2 or 3"); + } + } + + private static int GetIndexOffset(int numSubsets, int partitionIndex, int bitCount, int index) + { + if (index == 0) return 0; + if (numSubsets == 1) + { + return bitCount * index - 1; + } + if (numSubsets == 2) + { + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; + if (index <= anchorIndex) + { + return bitCount * index - 1; + } + else + { + return bitCount * index - 2; + } + } + throw new ArgumentOutOfRangeException(nameof(numSubsets), numSubsets, "Number of subsets can only be 1, 2 or 3"); + } + + /// + /// Decrements bitCount by one if index is one of the anchor indices. + /// + private static int GetIndexBitCount(int numSubsets, int partitionIndex, int bitCount, int index) + { + if (index == 0) return bitCount - 1; + if (numSubsets == 2) + { + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; + if (index == anchorIndex) + { + return bitCount - 1; + } + } + return bitCount; + } + + private readonly int GetIndexBegin() + { + return HasSubsets ? 82 : 65; + } + + internal readonly int GetColorIndex(int numSubsets, int partitionIndex, int bitCount, int index) + { + var indexOffset = GetIndexOffset(numSubsets, partitionIndex, bitCount, index); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); + var indexBegin = GetIndexBegin(); + return (int)ByteHelper.ExtractFrom128(lowBits, highBits, indexBegin + indexOffset, indexBitCount); + } + + internal static (int, int, int) InterpolateColor((int, int, int) endPointStart, (int, int, int) endPointEnd, + int colorIndex, int colorBitCount) + { + var result = ( + BptcEncodingHelpers.InterpolateInt(endPointStart.Item1, endPointEnd.Item1, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateInt(endPointStart.Item2, endPointEnd.Item2, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateInt(endPointStart.Item3, endPointEnd.Item3, colorIndex, colorBitCount) + ); + + return result; + } + + public readonly RawBlock4X4RgbFloat Decode(bool signed) + { + var output = new RawBlock4X4RgbFloat(); + var pixels = output.AsSpan; + + if (Type == Bc6BlockType.Unknown) + { + return ErrorBlock; + } + + var endpoints = ExtractRawEndpoints(signed); + var numSubsets = 1; + var partitionIndex = 0; + + if (HasSubsets) + { + numSubsets = 2; + partitionIndex = PartitionSetId; + } + + for (var i = 0; i < NumEndpoints; i++) + { + endpoints[i] = UnQuantize(endpoints[i], EndpointBits, signed); + } + + for (var i = 0; i < pixels.Length; i++) + { + var subsetIndex = GetPartitionIndex(numSubsets, partitionIndex, i); + + var endPointStart = endpoints[2 * subsetIndex]; + var endPointEnd = endpoints[2 * subsetIndex + 1]; + + var colorIndex = GetColorIndex(numSubsets, partitionIndex, ColorIndexBitCount, i); + + var (r, g, b) = FinishUnQuantize(InterpolateColor(endPointStart, endPointEnd, colorIndex, ColorIndexBitCount), signed); + + pixels[i] = new ColorRgbFloat(r, g, b); + } + + return output; + } + + #region pack methods + + private void StoreIndices(Span indices) + { + Debug.Assert(indices.Length == 16); + var numSubsets = HasSubsets ? 2 : 1; + var partSetId = PartitionSetId; + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(); + for (var i = 0; i < indices.Length; i++) + { + var partitionIndex = GetPartitionIndex(numSubsets, partSetId, i); + var indexOffset = GetIndexOffset(numSubsets, partitionIndex, colorBitCount, i); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + colorIndexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + private void StorePartitionSetId(int partitionSetId) + { + highBits = ByteHelper.Store5(highBits, 13, (byte)partitionSetId); + } + + public static Bc6Block PackType0((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 0; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType1((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 1; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType2((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 2; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType6((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 6; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType10((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 10; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType14((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 14; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType18((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 18; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType22((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 22; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType26((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 26; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType30((int, int, int) endpoint0, (int, int, int) endpoint1, + (int, int, int) endpoint2, (int, int, int) endpoint3, int partitionSetId, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 30; + block.StorePartitionSetId(partitionSetId); + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreEp2(endpoint2); + block.StoreEp3(endpoint3); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType3((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 3; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType7((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 7; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType11((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 11; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + public static Bc6Block PackType15((int, int, int) endpoint0, (int, int, int) endpoint1, Span indices) + { + var block = new Bc6Block(); + block.lowBits = 15; + block.StoreEp0(endpoint0); + block.StoreEp1(endpoint1); + block.StoreIndices(indices); + + return block; + } + + #endregion + + + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Bc7Block.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Bc7Block.cs new file mode 100644 index 0000000..4561291 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Bc7Block.cs @@ -0,0 +1,1260 @@ +using System.Diagnostics; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal enum Bc7BlockType : uint + { + Type0, + Type1, + Type2, + Type3, + Type4, + Type5, + Type6, + Type7, + Type8Reserved + } + + internal struct Bc7Block + { + public ulong lowBits; + public ulong highBits; + + + public static readonly int[][] Subsets2PartitionTable = { + new[] {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1}, + new[] {0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}, + new[] {0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + new[] {0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1}, + new[] {0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0}, + new[] {0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0}, + new[] {0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0}, + new[] {0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0}, + new[] {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1}, + new[] {0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0}, + new[] {0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0}, + new[] {0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0}, + new[] {0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0}, + new[] {0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1}, + new[] {0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0}, + new[] {0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0}, + new[] {0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0}, + new[] {0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0}, + new[] {0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1}, + new[] {0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0}, + new[] {0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0}, + new[] {0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0}, + new[] {0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0}, + new[] {0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0}, + new[] {0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1}, + new[] {0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0}, + new[] {0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0}, + new[] {0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1}, + new[] {0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1}, + new[] {0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new[] {0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0}, + new[] {0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0}, + new[] {0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1} + }; + + public static readonly int[][] Subsets3PartitionTable = { + new[] {0, 0, 1, 1, 0, 0, 1, 1, 0, 2, 2, 1, 2, 2, 2, 2}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 2, 2, 1, 1, 2, 2, 2, 1}, + new[] {0, 0, 0, 0, 2, 0, 0, 1, 2, 2, 1, 1, 2, 2, 1, 1}, + new[] {0, 2, 2, 2, 0, 0, 2, 2, 0, 0, 1, 1, 0, 1, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 1, 1, 2, 2}, + new[] {0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 2, 2}, + new[] {0, 0, 2, 2, 0, 0, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1}, + new[] {0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2}, + new[] {0, 0, 1, 2, 0, 0, 1, 2, 0, 0, 1, 2, 0, 0, 1, 2}, + new[] {0, 1, 1, 2, 0, 1, 1, 2, 0, 1, 1, 2, 0, 1, 1, 2}, + new[] {0, 1, 2, 2, 0, 1, 2, 2, 0, 1, 2, 2, 0, 1, 2, 2}, + new[] {0, 0, 1, 1, 0, 1, 1, 2, 1, 1, 2, 2, 1, 2, 2, 2}, + new[] {0, 0, 1, 1, 2, 0, 0, 1, 2, 2, 0, 0, 2, 2, 2, 0}, + new[] {0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 2, 1, 1, 2, 2}, + new[] {0, 1, 1, 1, 0, 0, 1, 1, 2, 0, 0, 1, 2, 2, 0, 0}, + new[] {0, 0, 0, 0, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2}, + new[] {0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 1, 1, 1, 1}, + new[] {0, 1, 1, 1, 0, 1, 1, 1, 0, 2, 2, 2, 0, 2, 2, 2}, + new[] {0, 0, 0, 1, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 2, 1}, + new[] {0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 2, 2, 0, 1, 2, 2}, + new[] {0, 0, 0, 0, 1, 1, 0, 0, 2, 2, 1, 0, 2, 2, 1, 0}, + new[] {0, 1, 2, 2, 0, 1, 2, 2, 0, 0, 1, 1, 0, 0, 0, 0}, + new[] {0, 0, 1, 2, 0, 0, 1, 2, 1, 1, 2, 2, 2, 2, 2, 2}, + new[] {0, 1, 1, 0, 1, 2, 2, 1, 1, 2, 2, 1, 0, 1, 1, 0}, + new[] {0, 0, 0, 0, 0, 1, 1, 0, 1, 2, 2, 1, 1, 2, 2, 1}, + new[] {0, 0, 2, 2, 1, 1, 0, 2, 1, 1, 0, 2, 0, 0, 2, 2}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 2, 0, 0, 2, 2, 2, 2, 2}, + new[] {0, 0, 1, 1, 0, 1, 2, 2, 0, 1, 2, 2, 0, 0, 1, 1}, + new[] {0, 0, 0, 0, 2, 0, 0, 0, 2, 2, 1, 1, 2, 2, 2, 1}, + new[] {0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 2, 2, 1, 2, 2, 2}, + new[] {0, 2, 2, 2, 0, 0, 2, 2, 0, 0, 1, 2, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 2, 2, 0, 2, 2, 2}, + new[] {0, 1, 2, 0, 0, 1, 2, 0, 0, 1, 2, 0, 0, 1, 2, 0}, + new[] {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 0, 0, 0, 0}, + new[] {0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0}, + new[] {0, 1, 2, 0, 2, 0, 1, 2, 1, 2, 0, 1, 0, 1, 2, 0}, + new[] {0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1}, + new[] {0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1}, + new[] {0, 1, 0, 1, 0, 1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 2, 1, 2, 1, 2, 1}, + new[] {0, 0, 2, 2, 1, 1, 2, 2, 0, 0, 2, 2, 1, 1, 2, 2}, + new[] {0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1}, + new[] {0, 2, 2, 0, 1, 2, 2, 1, 0, 2, 2, 0, 1, 2, 2, 1}, + new[] {0, 1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 0, 1}, + new[] {0, 0, 0, 0, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1}, + new[] {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 2, 2, 2, 2}, + new[] {0, 2, 2, 2, 0, 1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1}, + new[] {0, 0, 0, 2, 1, 1, 1, 2, 0, 0, 0, 2, 1, 1, 1, 2}, + new[] {0, 0, 0, 0, 2, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2}, + new[] {0, 2, 2, 2, 0, 1, 1, 1, 0, 1, 1, 1, 0, 2, 2, 2}, + new[] {0, 0, 0, 2, 1, 1, 1, 2, 1, 1, 1, 2, 0, 0, 0, 2}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 2, 2, 2, 2}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 2, 2, 1, 1, 2}, + new[] {0, 1, 1, 0, 0, 1, 1, 0, 2, 2, 2, 2, 2, 2, 2, 2}, + new[] {0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 2, 2}, + new[] {0, 0, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 0, 0, 2, 2}, + new[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 2}, + new[] {0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1}, + new[] {0, 2, 2, 2, 1, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2}, + new[] {0, 1, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}, + new[] {0, 1, 1, 1, 2, 0, 1, 1, 2, 2, 0, 1, 2, 2, 2, 0}, + }; + + public static readonly int[] Subsets2AnchorIndices = { + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 2, 8, 2, 2, 8, 8, 15, + 2, 8, 2, 2, 8, 8, 2, 2, + 15, 15, 6, 8, 2, 8, 15, 15, + 2, 8, 2, 2, 2, 15, 15, 6, + 6, 2, 6, 8, 15, 15, 2, 2, + 15, 15, 15, 15, 15, 2, 2, 15 + }; + + public static readonly int[] Subsets3AnchorIndices2 = { + 3, 3, 15, 15, 8, 3, 15, 15, + 8, 8, 6, 6, 6, 5, 3, 3, + 3, 3, 8, 15, 3, 3, 6, 10, + 5, 8, 8, 6, 8, 5, 15, 15, + 8, 15, 3, 5, 6, 10, 8, 15, + 15, 3, 15, 5, 15, 15, 15, 15, + 3, 15, 5, 5, 5, 8, 5, 10, + 5, 10, 8, 13, 15, 12, 3, 3 + }; + + public static readonly int[] Subsets3AnchorIndices3 = { + 15, 8, 8, 3, 15, 15, 3, 8, + 15, 15, 15, 15, 15, 15, 15, 8, + 15, 8, 15, 3, 15, 8, 15, 8, + 3, 15, 6, 10, 15, 15, 10, 8, + 15, 3, 15, 10, 10, 8, 9, 10, + 6, 15, 8, 15, 3, 6, 6, 8, + 15, 3, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 3, 15, 15, 8 + }; + + public static readonly RawBlock4X4Rgba32 ErrorBlock = new RawBlock4X4Rgba32(new ColorRgba32(255, 0, 255)); + + public Bc7BlockType Type + { + get + { + for (var i = 0; i < 8; i++) + { + var mask = (ulong)(1 << i); + if ((lowBits & mask) == mask) + { + return (Bc7BlockType)i; + } + } + + return Bc7BlockType.Type8Reserved; + } + } + + public int NumSubsets => Type switch + { + Bc7BlockType.Type0 => 3, + Bc7BlockType.Type1 => 2, + Bc7BlockType.Type2 => 3, + Bc7BlockType.Type3 => 2, + Bc7BlockType.Type7 => 2, + _ => 1 + }; + + public bool HasSubsets => Type switch + { + Bc7BlockType.Type0 => true, + Bc7BlockType.Type1 => true, + Bc7BlockType.Type2 => true, + Bc7BlockType.Type3 => true, + Bc7BlockType.Type7 => true, + _ => false + }; + + public int PartitionSetId => Type switch + { + Bc7BlockType.Type0 => ByteHelper.Extract4(lowBits, 1), + Bc7BlockType.Type1 => ByteHelper.Extract6(lowBits, 2), + Bc7BlockType.Type2 => ByteHelper.Extract6(lowBits, 3), + Bc7BlockType.Type3 => ByteHelper.Extract6(lowBits, 4), + Bc7BlockType.Type7 => ByteHelper.Extract6(lowBits, 8), + _ => -1 + }; + + public byte RotationBits => Type switch + { + Bc7BlockType.Type4 => ByteHelper.Extract2(lowBits, 5), + Bc7BlockType.Type5 => ByteHelper.Extract2(lowBits, 6), + _ => 0 + }; + + /// + /// Bitcount of color component including Pbit + /// + public int ColorComponentPrecision => Type switch + { + Bc7BlockType.Type0 => 5, + Bc7BlockType.Type1 => 7, + Bc7BlockType.Type2 => 5, + Bc7BlockType.Type3 => 8, + Bc7BlockType.Type4 => 5, + Bc7BlockType.Type5 => 7, + Bc7BlockType.Type6 => 8, + Bc7BlockType.Type7 => 6, + _ => 0 + }; + + /// + /// Bitcount of alpha component including Pbit + /// + public int AlphaComponentPrecision => Type switch + { + + Bc7BlockType.Type4 => 6, + Bc7BlockType.Type5 => 8, + Bc7BlockType.Type6 => 8, + Bc7BlockType.Type7 => 6, + _ => 0 + }; + + public bool HasRotationBits => Type switch + { + Bc7BlockType.Type4 => true, + Bc7BlockType.Type5 => true, + _ => false + }; + + public bool HasPBits => Type switch + { + Bc7BlockType.Type0 => true, + Bc7BlockType.Type1 => true, + Bc7BlockType.Type3 => true, + Bc7BlockType.Type6 => true, + Bc7BlockType.Type7 => true, + _ => false + }; + + public bool HasAlpha => Type switch + { + Bc7BlockType.Type4 => true, + Bc7BlockType.Type5 => true, + Bc7BlockType.Type6 => true, + Bc7BlockType.Type7 => true, + _ => false + }; + + /// + /// Type 4 has 2-bit and 3-bit indices. If index mode is 0, color components will use 2-bit indices and alpha will use 3-bit indices. + /// In index mode 1, color will use 3-bit indices and alpha will use 2-bit indices. + /// + public int Type4IndexMode => Type switch + { + Bc7BlockType.Type4 => ByteHelper.Extract1(lowBits, 7), + _ => 0 + }; + + public int ColorIndexBitCount => Type switch + { + Bc7BlockType.Type0 => 3, + Bc7BlockType.Type1 => 3, + Bc7BlockType.Type2 => 2, + Bc7BlockType.Type3 => 2, + Bc7BlockType.Type4 when Type4IndexMode == 0 => 2, + Bc7BlockType.Type4 when Type4IndexMode == 1 => 3, + Bc7BlockType.Type5 => 2, + Bc7BlockType.Type6 => 4, + Bc7BlockType.Type7 => 2, + _ => 0 + }; + + public int AlphaIndexBitCount => Type switch + { + Bc7BlockType.Type4 when Type4IndexMode == 0 => 3, + Bc7BlockType.Type4 when Type4IndexMode == 1 => 2, + Bc7BlockType.Type5 => 2, + Bc7BlockType.Type6 => 4, + Bc7BlockType.Type7 => 2, + _ => 0 + }; + + + + private void ExtractRawEndpoints(ColorRgba32[] endpoints) + { + switch (Type) + { + case Bc7BlockType.Type0: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 5, 4); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 9, 4); + endpoints[2].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 13, 4); + endpoints[3].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 17, 4); + endpoints[4].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 21, 4); + endpoints[5].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 25, 4); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 29, 4); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 33, 4); + endpoints[2].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 37, 4); + endpoints[3].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 41, 4); + endpoints[4].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 45, 4); + endpoints[5].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 49, 4); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 53, 4); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 57, 4); + endpoints[2].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 61, 4); + endpoints[3].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 65, 4); + endpoints[4].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 69, 4); + endpoints[5].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 73, 4); + break; + case Bc7BlockType.Type1: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 8, 6); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 14, 6); + endpoints[2].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 20, 6); + endpoints[3].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 26, 6); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 32, 6); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 38, 6); + endpoints[2].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 44, 6); + endpoints[3].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 50, 6); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 56, 6); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 62, 6); + endpoints[2].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 68, 6); + endpoints[3].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 74, 6); + break; + case Bc7BlockType.Type2: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 9, 5); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 14, 5); + endpoints[2].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 19, 5); + endpoints[3].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 24, 5); + endpoints[4].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 29, 5); + endpoints[5].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 34, 5); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 39, 5); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 44, 5); + endpoints[2].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 49, 5); + endpoints[3].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 54, 5); + endpoints[4].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 59, 5); + endpoints[5].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 64, 5); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 69, 5); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 74, 5); + endpoints[2].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 79, 5); + endpoints[3].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 84, 5); + endpoints[4].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 89, 5); + endpoints[5].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 94, 5); + break; + case Bc7BlockType.Type3: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 10, 7); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 17, 7); + endpoints[2].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 24, 7); + endpoints[3].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 31, 7); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 38, 7); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 45, 7); + endpoints[2].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 52, 7); + endpoints[3].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 59, 7); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 66, 7); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 73, 7); + endpoints[2].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 80, 7); + endpoints[3].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 87, 7); + break; + case Bc7BlockType.Type4: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 8, 5); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 13, 5); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 18, 5); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 23, 5); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 28, 5); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 33, 5); + + endpoints[0].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 38, 6); + endpoints[1].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 44, 6); + break; + case Bc7BlockType.Type5: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 8, 7); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 15, 7); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 22, 7); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 29, 7); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 36, 7); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 43, 7); + + endpoints[0].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 50, 8); + endpoints[1].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 58, 8); + break; + case Bc7BlockType.Type6: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 7, 7); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 14, 7); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 21, 7); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 28, 7); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 35, 7); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 42, 7); + + endpoints[0].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 49, 7); + endpoints[1].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 56, 7); + break; + case Bc7BlockType.Type7: + endpoints[0].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 14, 5); + endpoints[1].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 19, 5); + endpoints[2].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 24, 5); + endpoints[3].r = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 29, 5); + + endpoints[0].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 34, 5); + endpoints[1].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 39, 5); + endpoints[2].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 44, 5); + endpoints[3].g = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 49, 5); + + endpoints[0].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 54, 5); + endpoints[1].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 59, 5); + endpoints[2].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 64, 5); + endpoints[3].b = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 69, 5); + + endpoints[0].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 74, 5); + endpoints[1].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 79, 5); + endpoints[2].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 84, 5); + endpoints[3].a = (byte)ByteHelper.ExtractFrom128(lowBits, highBits, 89, 5); + break; + default: + throw new InvalidDataException(); + } + } + + private byte[] ExtractPBitArray() + { + switch (Type) + { + case Bc7BlockType.Type0: + return new[]{ + ByteHelper.Extract1(highBits, 77 - 64), + ByteHelper.Extract1(highBits, 78 - 64), + ByteHelper.Extract1(highBits, 79 - 64), + ByteHelper.Extract1(highBits, 80 - 64), + ByteHelper.Extract1(highBits, 81 - 64), + ByteHelper.Extract1(highBits, 82 - 64), + }; + case Bc7BlockType.Type1: + return new[]{ + ByteHelper.Extract1(highBits, 80 - 64), + ByteHelper.Extract1(highBits, 81 - 64) + }; + case Bc7BlockType.Type3: + return new[]{ + ByteHelper.Extract1(highBits, 94 - 64), + ByteHelper.Extract1(highBits, 95 - 64), + ByteHelper.Extract1(highBits, 96 - 64), + ByteHelper.Extract1(highBits, 97 - 64) + }; + case Bc7BlockType.Type6: + return new[]{ + ByteHelper.Extract1(lowBits, 63), + ByteHelper.Extract1(highBits, 0) + }; + case Bc7BlockType.Type7: + return new[]{ + ByteHelper.Extract1(highBits, 94 - 64), + ByteHelper.Extract1(highBits, 95 - 64), + ByteHelper.Extract1(highBits, 96 - 64), + ByteHelper.Extract1(highBits, 97 - 64) + }; + default: + return Array.Empty(); + } + } + + private void FinalizeEndpoints(ColorRgba32[] endpoints) + { + if (HasPBits) + { + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] <<= 1; + } + var pBits = ExtractPBitArray(); + + if (Type == Bc7BlockType.Type1) + { + endpoints[0] |= pBits[0]; + endpoints[1] |= pBits[0]; + endpoints[2] |= pBits[1]; + endpoints[3] |= pBits[1]; + } + else + { + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] |= pBits[i]; + } + } + } + + var colorPrecision = ColorComponentPrecision; + var alphaPrecision = AlphaComponentPrecision; + for (var i = 0; i < endpoints.Length; i++) + { + // ColorComponentPrecision & AlphaComponentPrecision includes pbit + // left shift endpoint components so that their MSB lies in bit 7 + endpoints[i].r = (byte)(endpoints[i].r << 8 - colorPrecision); + endpoints[i].g = (byte)(endpoints[i].g << 8 - colorPrecision); + endpoints[i].b = (byte)(endpoints[i].b << 8 - colorPrecision); + endpoints[i].a = (byte)(endpoints[i].a << 8 - alphaPrecision); + + // Replicate each component's MSB into the LSBs revealed by the left-shift operation above + endpoints[i].r = (byte)(endpoints[i].r | endpoints[i].r >> colorPrecision); + endpoints[i].g = (byte)(endpoints[i].g | endpoints[i].g >> colorPrecision); + endpoints[i].b = (byte)(endpoints[i].b | endpoints[i].b >> colorPrecision); + endpoints[i].a = (byte)(endpoints[i].a | endpoints[i].a >> alphaPrecision); + } + + //If this mode does not explicitly define the alpha component + //set alpha equal to 255 + if (!HasAlpha) + { + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i].a = 255; + } + } + } + + public ColorRgba32[] ExtractEndpoints() + { + var endpoints = new ColorRgba32[NumSubsets * 2]; + ExtractRawEndpoints(endpoints); + FinalizeEndpoints(endpoints); + return endpoints; + } + + private int GetPartitionIndex(int numSubsets, int partitionSetId, int i) + { + switch (numSubsets) + { + case 1: + return 0; + case 2: + return Subsets2PartitionTable[partitionSetId][i]; + case 3: + return Subsets3PartitionTable[partitionSetId][i]; + default: + throw new ArgumentOutOfRangeException(nameof(numSubsets), numSubsets, "Number of subsets can only be 1, 2 or 3"); + } + } + + private int GetIndexOffset(Bc7BlockType type, int numSubsets, int partitionIndex, int bitCount, int index) + { + if (index == 0) return 0; + if (numSubsets == 1) + { + return bitCount * index - 1; + } + if (numSubsets == 2) + { + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; + if (index <= anchorIndex) + { + return bitCount * index - 1; + } + else + { + return bitCount * index - 2; + } + } + if (numSubsets == 3) + { + var anchor2Index = Subsets3AnchorIndices2[partitionIndex]; + var anchor3Index = Subsets3AnchorIndices3[partitionIndex]; + + if (index <= anchor2Index && index <= anchor3Index) + { + return bitCount * index - 1; + } + else if (index > anchor2Index && index > anchor3Index) + { + return bitCount * index - 3; + } + else + { + return bitCount * index - 2; + } + } + throw new ArgumentOutOfRangeException(nameof(numSubsets), numSubsets, "Number of subsets can only be 1, 2 or 3"); + } + + /// + /// Decrements bitCount by one if index is one of the anchor indices. + /// + private int GetIndexBitCount(int numSubsets, int partitionIndex, int bitCount, int index) + { + if (index == 0) return bitCount - 1; + if (numSubsets == 2) + { + var anchorIndex = Subsets2AnchorIndices[partitionIndex]; + if (index == anchorIndex) + { + return bitCount - 1; + } + } + else if (numSubsets == 3) + { + var anchor2Index = Subsets3AnchorIndices2[partitionIndex]; + var anchor3Index = Subsets3AnchorIndices3[partitionIndex]; + if (index == anchor2Index) + { + return bitCount - 1; + } + if (index == anchor3Index) + { + return bitCount - 1; + } + } + return bitCount; + } + + private int GetIndexBegin(Bc7BlockType type, int bitCount, bool isAlpha) + { + switch (type) + { + case Bc7BlockType.Type0: + return 83; + case Bc7BlockType.Type1: + return 82; + case Bc7BlockType.Type2: + return 99; + case Bc7BlockType.Type3: + return 98; + case Bc7BlockType.Type4: + if (bitCount == 2) + { + return 50; + } + else + { + return 81; + } + case Bc7BlockType.Type5: + if (isAlpha) + { + return 97; + } + else + { + return 66; + } + case Bc7BlockType.Type6: + return 65; + case Bc7BlockType.Type7: + return 98; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + private int GetAlphaIndex(Bc7BlockType type, int numSubsets, int partitionIndex, int bitCount, int index) + { + if (bitCount == 0) return 0; // No Alpha + var indexOffset = GetIndexOffset(type, numSubsets, partitionIndex, bitCount, index); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); + var indexBegin = GetIndexBegin(type, bitCount, true); + return (int)ByteHelper.ExtractFrom128(lowBits, highBits, indexBegin + indexOffset, indexBitCount); + } + + private int GetColorIndex(Bc7BlockType type, int numSubsets, int partitionIndex, int bitCount, int index) + { + var indexOffset = GetIndexOffset(type, numSubsets, partitionIndex, bitCount, index); + var indexBitCount = GetIndexBitCount(numSubsets, partitionIndex, bitCount, index); + var indexBegin = GetIndexBegin(type, bitCount, false); + return (int)ByteHelper.ExtractFrom128(lowBits, highBits, indexBegin + indexOffset, indexBitCount); + } + + private ColorRgba32 InterpolateColor(ColorRgba32 endPointStart, ColorRgba32 endPointEnd, + int colorIndex, int alphaIndex, int colorBitCount, int alphaBitCount) + { + var result = new ColorRgba32( + BptcEncodingHelpers.InterpolateByte(endPointStart.r, endPointEnd.r, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.g, endPointEnd.g, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.b, endPointEnd.b, colorIndex, colorBitCount), + BptcEncodingHelpers.InterpolateByte(endPointStart.a, endPointEnd.a, alphaIndex, alphaBitCount) + ); + + return result; + } + /// + /// 00 – no swapping + /// 01 – swap A and R + /// 10 – swap A and G + /// 11 - swap A and B + /// + private static ColorRgba32 SwapChannels(ColorRgba32 source, int rotation) + { + switch (rotation) + { + case 0b00: + return source; + case 0b01: + return new ColorRgba32(source.a, source.g, source.b, source.r); + case 0b10: + return new ColorRgba32(source.r, source.a, source.b, source.g); + case 0b11: + return new ColorRgba32(source.r, source.g, source.a, source.b); + default: + return source; + } + } + + + public RawBlock4X4Rgba32 Decode() + { + var output = new RawBlock4X4Rgba32(); + var type = Type; + + if (type == Bc7BlockType.Type8Reserved) + { + return ErrorBlock; + } + + ////decode partition data from explicit partition bits + //subset_index = 0; + var numSubsets = 1; + var partitionIndex = 0; + + if (HasSubsets) + { + numSubsets = NumSubsets; + partitionIndex = PartitionSetId; + } + + var pixels = output.AsSpan; + + var hasRotationBits = HasRotationBits; + int rotation = RotationBits; + + var endpoints = ExtractEndpoints(); + for (var i = 0; i < pixels.Length; i++) + { + var subsetIndex = GetPartitionIndex(numSubsets, partitionIndex, i); + + var endPointStart = endpoints[2 * subsetIndex]; + var endPointEnd = endpoints[2 * subsetIndex + 1]; + + var alphaBitCount = AlphaIndexBitCount; + var colorBitCount = ColorIndexBitCount; + var alphaIndex = GetAlphaIndex(type, numSubsets, partitionIndex, alphaBitCount, i); + var colorIndex = GetColorIndex(type, numSubsets, partitionIndex, colorBitCount, i); + + var outputColor = InterpolateColor(endPointStart, endPointEnd, colorIndex, alphaIndex, + colorBitCount, alphaBitCount); + + if (hasRotationBits) + { + //Decode the 2 color rotation bits as follows: + // 00 – Block Format is Scalar(A) Vector(RGB) - no swapping + // 01 – Block Format is Scalar(R) Vector(AGB) - swap A and R + // 10 – Block Format is Scalar(G) Vector(RAB) - swap A and G + // 11 - Block Format is Scalar(B) Vector(RGA) - swap A and B + outputColor = SwapChannels(outputColor, rotation); + } + + pixels[i] = outputColor; + } + + return output; + } + + + public void PackType0(int partitionIndex4Bit, byte[][] subsetEndpoints, byte[] pBits, byte[] indices) + { + Debug.Assert(partitionIndex4Bit < 16, "Mode 0 should have 4bit partition index"); + Debug.Assert(subsetEndpoints.Length == 6, "Mode 0 should have 6 endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.Length == 3), "Mode 0 should have RGB endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 4)), "Mode 0 should have 4bit endpoints"); + Debug.Assert(pBits.Length == 6, "Mode 0 should have 6 pBits"); + Debug.Assert(indices.Length == 16, "Provide 16 indices"); + Debug.Assert(indices.All(x => x < 1 << 3), "Mode 0 should have 3bit indices"); + + lowBits = 1; // Set Mode 0 + highBits = 0; + + lowBits = ByteHelper.Store4(lowBits, 1, (byte)partitionIndex4Bit); + + var nextIdx = 5; + //Store endpoints + for (var i = 0; i < subsetEndpoints[0].Length; i++) + { + for (var j = 0; j < subsetEndpoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 4, subsetEndpoints[j][i]); + nextIdx += 4; + } + } + //Store pBits + for (var i = 0; i < pBits.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); + nextIdx++; + } + Debug.Assert(nextIdx == 83); + + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type0, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type0, NumSubsets, + partitionIndex4Bit, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + partitionIndex4Bit, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + indexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + public void PackType1(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] pBits, byte[] indices) + { + Debug.Assert(partitionIndex6Bit < 64, "Mode 1 should have 6bit partition index"); + Debug.Assert(subsetEndpoints.Length == 4, "Mode 1 should have 4 endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.Length == 3), "Mode 1 should have RGB endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 6)), "Mode 1 should have 6bit endpoints"); + Debug.Assert(pBits.Length == 2, "Mode 1 should have 2 pBits"); + Debug.Assert(indices.Length == 16, "Provide 16 indices"); + Debug.Assert(indices.All(x => x < 1 << 3), "Mode 1 should have 3bit indices"); + + lowBits = 2; // Set Mode 1 + highBits = 0; + + lowBits = ByteHelper.Store6(lowBits, 2, (byte)partitionIndex6Bit); + + var nextIdx = 8; + //Store endpoints + for (var i = 0; i < subsetEndpoints[0].Length; i++) + { + for (var j = 0; j < subsetEndpoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 6, subsetEndpoints[j][i]); + nextIdx += 6; + } + } + //Store pBits + for (var i = 0; i < pBits.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, 1, pBits[i]); + nextIdx++; + } + Debug.Assert(nextIdx == 82); + + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type1, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type1, NumSubsets, + partitionIndex6Bit, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + partitionIndex6Bit, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + indexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + public void PackType2(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] indices) + { + Debug.Assert(partitionIndex6Bit < 64, "Mode 2 should have 6bit partition index"); + Debug.Assert(subsetEndpoints.Length == 6, "Mode 2 should have 6 endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.Length == 3), "Mode 2 should have RGB endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 5)), "Mode 2 should have 5bit endpoints"); + Debug.Assert(indices.Length == 16, "Provide 16 indices"); + Debug.Assert(indices.All(x => x < 1 << 2), "Mode 2 should have 2bit indices"); + + lowBits = 0b100; // Set Mode 2 + highBits = 0; + + lowBits = ByteHelper.Store6(lowBits, 3, (byte)partitionIndex6Bit); + + var nextIdx = 9; + //Store endpoints + for (var i = 0; i < subsetEndpoints[0].Length; i++) + { + for (var j = 0; j < subsetEndpoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + nextIdx, 5, subsetEndpoints[j][i]); + nextIdx += 5; + } + } + Debug.Assert(nextIdx == 99); + + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type2, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type2, NumSubsets, + partitionIndex6Bit, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + partitionIndex6Bit, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + indexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + public void PackType3(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] pBits, byte[] indices) + { + Debug.Assert(partitionIndex6Bit < 64, "Mode 3 should have 6bit partition index"); + Debug.Assert(subsetEndpoints.Length == 4, "Mode 3 should have 4 endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.Length == 3), "Mode 3 should have RGB endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 7)), "Mode 3 should have 7bit endpoints"); + Debug.Assert(pBits.Length == 4, "Mode 3 should have 4 pBits"); + Debug.Assert(indices.Length == 16, "Provide 16 indices"); + Debug.Assert(indices.All(x => x < 1 << 2), "Mode 3 should have 2bit indices"); + + lowBits = 0b1000; // Set Mode 3 + highBits = 0; + + lowBits = ByteHelper.Store6(lowBits, 4, (byte)partitionIndex6Bit); + + var nextIdx = 10; + //Store endpoints + for (var i = 0; i < subsetEndpoints[0].Length; i++) + { + for (var j = 0; j < subsetEndpoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + nextIdx, 7, subsetEndpoints[j][i]); + nextIdx += 7; + } + } + //Store pBits + for (var i = 0; i < pBits.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, + 1, pBits[i]); + nextIdx++; + } + Debug.Assert(nextIdx == 98); + + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type3, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type3, NumSubsets, + partitionIndex6Bit, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + partitionIndex6Bit, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + indexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + public void PackType4(int rotation, byte idxMode, byte[][] colorEndPoints, byte[] alphaEndPoints, byte[] indices2Bit, byte[] indices3Bit) + { + Debug.Assert(rotation < 4, "Rotation can only be 0-3"); + Debug.Assert(idxMode < 2, "IndexMode can only be 0 or 1"); + Debug.Assert(colorEndPoints.Length == 2, "Mode 4 should have 2 endpoints"); + Debug.Assert(colorEndPoints.All(x => x.Length == 3), "Mode 4 should have RGB color endpoints"); + Debug.Assert(colorEndPoints.All(x => x.All(y => y < 1 << 5)), "Mode 4 should have 5bit color endpoints"); + Debug.Assert(alphaEndPoints.Length == 2, "Mode 4 should have 2 endpoints"); + Debug.Assert(alphaEndPoints.All(x => x < 1 << 6), "Mode 4 should have 6bit alpha endpoints"); + + Debug.Assert(indices2Bit.Length == 16, "Provide 16 indices"); + Debug.Assert(indices2Bit.All(x => x < 1 << 2), "Mode 4 should have 2bit indices"); + Debug.Assert(indices3Bit.Length == 16, "Provide 16 indices"); + Debug.Assert(indices3Bit.All(x => x < 1 << 3), "Mode 4 should have 3bit indices"); + + lowBits = 0b10000; // Set Mode 4 + highBits = 0; + + lowBits = ByteHelper.Store2(lowBits, 5, (byte)rotation); + lowBits = ByteHelper.Store1(lowBits, 7, idxMode); + + var nextIdx = 8; + //Store color endpoints + for (var i = 0; i < colorEndPoints[0].Length; i++) + { + for (var j = 0; j < colorEndPoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + nextIdx, 5, colorEndPoints[j][i]); + nextIdx += 5; + } + } + //Store alpha endpoints + for (var i = 0; i < alphaEndPoints.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, + 6, alphaEndPoints[i]); + nextIdx += 6; + } + Debug.Assert(nextIdx == 50); + + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(Bc7BlockType.Type4, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type4, NumSubsets, + 0, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + 0, colorBitCount, i); + + if (idxMode == 0) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + colorIndexBegin + indexOffset, indexBitCount, indices2Bit[i]); + } + else + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + colorIndexBegin + indexOffset, indexBitCount, indices3Bit[i]); + } + } + + var alphaBitCount = AlphaIndexBitCount; + var alphaIndexBegin = GetIndexBegin(Bc7BlockType.Type4, alphaBitCount, true); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type4, NumSubsets, + 0, alphaBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + 0, alphaBitCount, i); + + if (idxMode == 0) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + alphaIndexBegin + indexOffset, indexBitCount, indices3Bit[i]); + } + else + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + alphaIndexBegin + indexOffset, indexBitCount, indices2Bit[i]); + } + } + } + + public void PackType5(int rotation, byte[][] colorEndPoints, byte[] alphaEndPoints, byte[] colorIndices, byte[] alphaIndices) + { + Debug.Assert(rotation < 4, "Rotation can only be 0-3"); + Debug.Assert(colorEndPoints.Length == 2, "Mode 5 should have 2 endpoints"); + Debug.Assert(colorEndPoints.All(x => x.Length == 3), "Mode 5 should have RGB color endpoints"); + Debug.Assert(colorEndPoints.All(x => x.All(y => y < 1 << 7)), "Mode 5 should have 7bit color endpoints"); + Debug.Assert(alphaEndPoints.Length == 2, "Mode 5 should have 2 endpoints"); + + Debug.Assert(colorIndices.Length == 16, "Provide 16 indices"); + Debug.Assert(colorIndices.All(x => x < 1 << 2), "Mode 5 should have 2bit color indices"); + Debug.Assert(alphaIndices.Length == 16, "Provide 16 indices"); + Debug.Assert(alphaIndices.All(x => x < 1 << 2), "Mode 5 should have 2bit alpha indices"); + + lowBits = 0b100000; // Set Mode 5 + highBits = 0; + + lowBits = ByteHelper.Store2(lowBits, 6, (byte)rotation); + + var nextIdx = 8; + //Store color endpoints + for (var i = 0; i < colorEndPoints[0].Length; i++) + { + for (var j = 0; j < colorEndPoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + nextIdx, 7, colorEndPoints[j][i]); + nextIdx += 7; + } + } + //Store alpha endpoints + for (var i = 0; i < alphaEndPoints.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, + 8, alphaEndPoints[i]); + nextIdx += 8; + } + Debug.Assert(nextIdx == 66); + + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(Bc7BlockType.Type5, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type5, NumSubsets, + 0, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + 0, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + colorIndexBegin + indexOffset, indexBitCount, colorIndices[i]); + + } + + var alphaBitCount = AlphaIndexBitCount; + var alphaIndexBegin = GetIndexBegin(Bc7BlockType.Type5, alphaBitCount, true); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type5, NumSubsets, + 0, alphaBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + 0, alphaBitCount, i); + + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + alphaIndexBegin + indexOffset, indexBitCount, alphaIndices[i]); + + } + } + + public void PackType6(byte[][] colorAlphaEndPoints, byte[] pBits, byte[] indices) + { + Debug.Assert(colorAlphaEndPoints.Length == 2, + "Mode 6 should have 2 endpoints"); + Debug.Assert(colorAlphaEndPoints.All(x => x.Length == 4), + "Mode 6 should have RGBA color endpoints"); + Debug.Assert(colorAlphaEndPoints.All(x => x.All(y => y < 1 << 7)), + "Mode 6 should have 7bit color and alpha endpoints"); + Debug.Assert(pBits.Length == 2, "Mode 6 should have 2 pBits"); + Debug.Assert(indices.Length == 16, "Provide 16 indices"); + Debug.Assert(indices.All(x => x < 1 << 4), "Mode 6 should have 4bit color indices"); + + lowBits = 0b1000000; // Set Mode 6 + highBits = 0; + + var nextIdx = 7; + //Store color and alpha endpoints + for (var i = 0; i < colorAlphaEndPoints[0].Length; i++) + { + for (var j = 0; j < colorAlphaEndPoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + nextIdx, 7, colorAlphaEndPoints[j][i]); + nextIdx += 7; + } + } + //Store pBits + for (var i = 0; i < pBits.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, + 1, pBits[i]); + nextIdx++; + } + Debug.Assert(nextIdx == 65); + + var colorBitCount = ColorIndexBitCount; + var colorIndexBegin = GetIndexBegin(Bc7BlockType.Type6, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type6, NumSubsets, + 0, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + 0, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + colorIndexBegin + indexOffset, indexBitCount, indices[i]); + } + } + + public void PackType7(int partitionIndex6Bit, byte[][] subsetEndpoints, byte[] pBits, byte[] indices) + { + Debug.Assert(partitionIndex6Bit < 64, "Mode 7 should have 6bit partition index"); + Debug.Assert(subsetEndpoints.Length == 4, "Mode 7 should have 4 endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.Length == 4), "Mode 7 should have RGBA endpoints"); + Debug.Assert(subsetEndpoints.All(x => x.All(y => y < 1 << 5)), "Mode 7 should have 5bit endpoints"); + Debug.Assert(pBits.Length == 4, "Mode 7 should have 4 pBits"); + Debug.Assert(indices.Length == 16, "Provide 16 indices"); + Debug.Assert(indices.All(x => x < 1 << 2), "Mode 3 should have 2bit indices"); + + lowBits = 0b10000000; // Set Mode 7 + highBits = 0; + + lowBits = ByteHelper.Store6(lowBits, 8, (byte)partitionIndex6Bit); + + var nextIdx = 14; + //Store endpoints + for (var i = 0; i < subsetEndpoints[0].Length; i++) + { + for (var j = 0; j < subsetEndpoints.Length; j++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + nextIdx, 5, subsetEndpoints[j][i]); + nextIdx += 5; + } + } + //Store pBits + for (var i = 0; i < pBits.Length; i++) + { + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, nextIdx, + 1, pBits[i]); + nextIdx++; + } + Debug.Assert(nextIdx == 98); + + var colorBitCount = ColorIndexBitCount; + var indexBegin = GetIndexBegin(Bc7BlockType.Type7, colorBitCount, false); + for (var i = 0; i < 16; i++) + { + var indexOffset = GetIndexOffset(Bc7BlockType.Type7, NumSubsets, + partitionIndex6Bit, colorBitCount, i); + var indexBitCount = GetIndexBitCount(NumSubsets, + partitionIndex6Bit, colorBitCount, i); + + (lowBits, highBits) = ByteHelper.StoreTo128(lowBits, highBits, + indexBegin + indexOffset, indexBitCount, indices[i]); + } + } + } + +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/BinaryReaderWriterExtensions.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/BinaryReaderWriterExtensions.cs new file mode 100644 index 0000000..0fb7a25 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/BinaryReaderWriterExtensions.cs @@ -0,0 +1,43 @@ +using System.Runtime.CompilerServices; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class BinaryReaderWriterExtensions + { + public static unsafe void WriteStruct(this BinaryWriter bw, T t) where T : unmanaged + { + var size = Unsafe.SizeOf(); + var bytes = stackalloc byte[size]; + Unsafe.Write(bytes, t); + var bSpan = new Span(bytes, size); + bw.Write(bSpan); + } + + public static unsafe T ReadStruct(this BinaryReader br) where T : unmanaged + { + var size = Unsafe.SizeOf(); + var bytes = stackalloc byte[size]; + var bSpan = new Span(bytes, size); + br.Read(bSpan); + return Unsafe.Read(bytes); + } + + public static void AddPadding(this BinaryWriter bw, uint padding) + { + for (var i = 0; i < padding; i++) + { + bw.Write((byte)0); + } + } + public static void AddPadding(this BinaryWriter bw, int padding) + => bw.AddPadding((uint)padding); + + public static void SkipPadding(this BinaryReader br, uint padding) + { + br.BaseStream.Seek(padding, SeekOrigin.Current); + } + + public static void SkipPadding(this BinaryReader br, int padding) + => br.SkipPadding((uint)padding); + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ByteHelper.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ByteHelper.cs new file mode 100644 index 0000000..df13945 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ByteHelper.cs @@ -0,0 +1,195 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class ByteHelper + { + public static byte ClampToByte(int i) + { + if (i < 0) i = 0; + if (i > 255) i = 255; + return (byte)i; + } + + public static byte ClampToByte(float f) + => ClampToByte((int)f); + + public static byte Extract1(ulong source, int index) + { + const ulong mask = 0b1UL; + return (byte)(source >> index & mask); + } + + public static ulong Store1(ulong dest, int index, byte value) + { + const ulong mask = 0b1UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract2(ulong source, int index) + { + const ulong mask = 0b11UL; + return (byte)(source >> index & mask); + } + + public static ulong Store2(ulong dest, int index, byte value) + { + const ulong mask = 0b11UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract3(ulong source, int index) + { + const ulong mask = 0b111UL; + return (byte)(source >> index & mask); + } + + public static ulong Store3(ulong dest, int index, byte value) + { + const ulong mask = 0b111UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract4(ulong source, int index) + { + const ulong mask = 0b1111UL; + return (byte)(source >> index & mask); + } + + public static ulong Store4(ulong dest, int index, byte value) + { + const ulong mask = 0b1111UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract5(ulong source, int index) + { + const ulong mask = 0b1_1111UL; + return (byte)(source >> index & mask); + } + + public static ulong Store5(ulong dest, int index, byte value) + { + const ulong mask = 0b1_1111UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract6(ulong source, int index) + { + const ulong mask = 0b11_1111UL; + return (byte)(source >> index & mask); + } + + public static ulong Store6(ulong dest, int index, byte value) + { + const ulong mask = 0b11_1111UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract7(ulong source, int index) + { + const ulong mask = 0b111_1111UL; + return (byte)(source >> index & mask); + } + + public static ulong Store7(ulong dest, int index, byte value) + { + const ulong mask = 0b111_1111UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static byte Extract8(ulong source, int index) + { + const ulong mask = 0b1111_1111UL; + return (byte)(source >> index & mask); + } + + public static ulong Store8(ulong dest, int index, byte value) + { + const ulong mask = 0b1111_1111UL; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + + public static ulong Extract(ulong source, int index, int bitCount) + { + unchecked + { + var mask = (0b1UL << bitCount) - 1; + return source >> index & mask; + } + } + + public static ulong Store(ulong dest, int index, int bitCount, ulong value) + { + unchecked + { + var mask = (0b1UL << bitCount) - 1; + dest &= ~(mask << index); + dest |= (value & mask) << index; + return dest; + } + } + + public static ulong ExtractFrom128(ulong low, ulong high, int index, int bitCount) + { + if (index + bitCount <= 64) + { // Extract from low + return Extract(low, index, bitCount); + } + else if (index >= 64) + { // Extract from high + return Extract(high, index - 64, bitCount); + } + else + { //handle boundary case + var lowIndex = index; + var lowBitCount = 64 - index; + var highBitCount = bitCount - lowBitCount; + var highIndex = 0; + + var value = Extract(low, lowIndex, lowBitCount); + var hVal = Extract(high, highIndex, highBitCount); + value = Store(value, lowBitCount, highBitCount, hVal); + return value; + } + } + + public static (ulong, ulong) StoreTo128(ulong low, ulong high, int index, int bitCount, ulong value) + { + if (index + bitCount <= 64) + { // Store to low + return (Store(low, index, bitCount, value), high); + } + else if (index >= 64) + { // Store to high + return (low, Store(high, index - 64, bitCount, value)); + } + else + { //handle boundary case + var lowIndex = index; + var lowBitCount = 64 - index; + var highBitCount = bitCount - lowBitCount; + var highIndex = 0; + + var l = Store(low, lowIndex, lowBitCount, value); + value >>= lowBitCount; + var h = Store(high, highIndex, highBitCount, value); + return (l, h); + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ColorComponent.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ColorComponent.cs new file mode 100644 index 0000000..2c72532 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ColorComponent.cs @@ -0,0 +1,33 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// The component to take from colors for BC4 and BC5. + /// + public enum ColorComponent + { + /// + /// The red component of an Rgba32 color. + /// + R, + + /// + /// The green component of an Rgba32 color. + /// + G, + + /// + /// The blue component of an Rgba32 color. + /// + B, + + /// + /// The alpha component of an Rgba32 color. + /// + A, + + /// + /// Use the color's luminance value as the component. + /// + Luminance + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Colors.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Colors.cs new file mode 100644 index 0000000..13b12b2 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Colors.cs @@ -0,0 +1,1337 @@ +using System.Numerics; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public struct ColorRgba32 : IEquatable + { + public byte r, g, b, a; + + public ColorRgba32(byte r, byte g, byte b, byte a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public ColorRgba32(byte r, byte g, byte b) + { + this.r = r; + this.g = g; + this.b = b; + a = 255; + } + + public bool Equals(ColorRgba32 other) + { + return r == other.r && g == other.g && b == other.b && a == other.a; + } + + public override bool Equals(object obj) + { + return obj is ColorRgba32 other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = hashCode * 397 ^ g.GetHashCode(); + hashCode = hashCode * 397 ^ b.GetHashCode(); + hashCode = hashCode * 397 ^ a.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgba32 left, ColorRgba32 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgba32 left, ColorRgba32 right) + { + return !left.Equals(right); + } + + public static ColorRgba32 operator +(ColorRgba32 left, ColorRgba32 right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r + right.r), + ByteHelper.ClampToByte(left.g + right.g), + ByteHelper.ClampToByte(left.b + right.b), + ByteHelper.ClampToByte(left.a + right.a)); + } + + public static ColorRgba32 operator -(ColorRgba32 left, ColorRgba32 right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r - right.r), + ByteHelper.ClampToByte(left.g - right.g), + ByteHelper.ClampToByte(left.b - right.b), + ByteHelper.ClampToByte(left.a - right.a)); + } + + public static ColorRgba32 operator /(ColorRgba32 left, double right) + { + return new ColorRgba32( + ByteHelper.ClampToByte((int)(left.r / right)), + ByteHelper.ClampToByte((int)(left.g / right)), + ByteHelper.ClampToByte((int)(left.b / right)), + ByteHelper.ClampToByte((int)(left.a / right)) + ); + } + + public static ColorRgba32 operator *(ColorRgba32 left, double right) + { + return new ColorRgba32( + ByteHelper.ClampToByte((int)(left.r * right)), + ByteHelper.ClampToByte((int)(left.g * right)), + ByteHelper.ClampToByte((int)(left.b * right)), + ByteHelper.ClampToByte((int)(left.a * right)) + ); + } + + /// + /// Component-wise left shift + /// + public static ColorRgba32 operator <<(ColorRgba32 left, int right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r << right), + ByteHelper.ClampToByte(left.g << right), + ByteHelper.ClampToByte(left.b << right), + ByteHelper.ClampToByte(left.a << right) + ); + } + + /// + /// Component-wise right shift + /// + public static ColorRgba32 operator >>(ColorRgba32 left, int right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r >> right), + ByteHelper.ClampToByte(left.g >> right), + ByteHelper.ClampToByte(left.b >> right), + ByteHelper.ClampToByte(left.a >> right) + ); + } + + /// + /// Component-wise bitwise OR operation + /// + public static ColorRgba32 operator |(ColorRgba32 left, ColorRgba32 right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r | right.r), + ByteHelper.ClampToByte(left.g | right.g), + ByteHelper.ClampToByte(left.b | right.b), + ByteHelper.ClampToByte(left.a | right.a) + ); + } + + /// + /// Component-wise bitwise OR operation + /// + public static ColorRgba32 operator |(ColorRgba32 left, int right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r | right), + ByteHelper.ClampToByte(left.g | right), + ByteHelper.ClampToByte(left.b | right), + ByteHelper.ClampToByte(left.a | right) + ); + } + + /// + /// Component-wise bitwise AND operation + /// + public static ColorRgba32 operator &(ColorRgba32 left, ColorRgba32 right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r & right.r), + ByteHelper.ClampToByte(left.g & right.g), + ByteHelper.ClampToByte(left.b & right.b), + ByteHelper.ClampToByte(left.a & right.a) + ); + } + + /// + /// Component-wise bitwise AND operation + /// + public static ColorRgba32 operator &(ColorRgba32 left, int right) + { + return new ColorRgba32( + ByteHelper.ClampToByte(left.r & right), + ByteHelper.ClampToByte(left.g & right), + ByteHelper.ClampToByte(left.b & right), + ByteHelper.ClampToByte(left.a & right) + ); + } + + public override string ToString() + { + return $"r : {r} g : {g} b : {b} a : {a}"; + } + + internal readonly ColorRgbaFloat ToFloat() + { + return new ColorRgbaFloat(this); + } + + public readonly ColorRgbFloat ToRgbFloat() + { + return new ColorRgbFloat(this); + } + } + + internal struct ColorRgbaFloat : IEquatable + { + public float r, g, b, a; + + public ColorRgbaFloat(float r, float g, float b, float a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public ColorRgbaFloat(ColorRgba32 other) + { + r = other.r / 255f; + g = other.g / 255f; + b = other.b / 255f; + a = other.a / 255f; + } + + public ColorRgbaFloat(float r, float g, float b) + { + this.r = r; + this.g = g; + this.b = b; + a = 1; + } + + public bool Equals(ColorRgbaFloat other) + { + return r == other.r && g == other.g && b == other.b && a == other.a; + } + + public override bool Equals(object obj) + { + return obj is ColorRgbaFloat other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = hashCode * 397 ^ g.GetHashCode(); + hashCode = hashCode * 397 ^ b.GetHashCode(); + hashCode = hashCode * 397 ^ a.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgbaFloat left, ColorRgbaFloat right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgbaFloat left, ColorRgbaFloat right) + { + return !left.Equals(right); + } + + public static ColorRgbaFloat operator +(ColorRgbaFloat left, ColorRgbaFloat right) + { + return new ColorRgbaFloat( + left.r + right.r, + left.g + right.g, + left.b + right.b, + left.a + right.a); + } + + public static ColorRgbaFloat operator -(ColorRgbaFloat left, ColorRgbaFloat right) + { + return new ColorRgbaFloat( + left.r - right.r, + left.g - right.g, + left.b - right.b, + left.a - right.a); + } + + public static ColorRgbaFloat operator /(ColorRgbaFloat left, float right) + { + return new ColorRgbaFloat( + left.r / right, + left.g / right, + left.b / right, + left.a / right + ); + } + + public static ColorRgbaFloat operator *(ColorRgbaFloat left, float right) + { + return new ColorRgbaFloat( + left.r * right, + left.g * right, + left.b * right, + left.a * right + ); + } + + public static ColorRgbaFloat operator *(float left, ColorRgbaFloat right) + { + return new ColorRgbaFloat( + right.r * left, + right.g * left, + right.b * left, + right.a * left + ); + } + + public override string ToString() + { + return $"r : {r:0.00} g : {g:0.00} b : {b:0.00} a : {a:0.00}"; + } + + public ColorRgba32 ToRgba32() + { + return new ColorRgba32( + ByteHelper.ClampToByte(r * 255), + ByteHelper.ClampToByte(g * 255), + ByteHelper.ClampToByte(b * 255), + ByteHelper.ClampToByte(a * 255) + ); + } + + } + + public struct ColorRgbFloat : IEquatable + { + public float r, g, b; + + public ColorRgbFloat(float r, float g, float b) + { + this.r = r; + this.g = g; + this.b = b; + } + + public ColorRgbFloat(ColorRgba32 other) + { + r = other.r / 255f; + g = other.g / 255f; + b = other.b / 255f; + } + + public ColorRgbFloat(Vector3 vector) + { + r = vector.X; + g = vector.Y; + b = vector.Z; + } + + public bool Equals(ColorRgbFloat other) + { + return r == other.r && g == other.g && b == other.b; + } + + public override bool Equals(object obj) + { + return obj is ColorRgbFloat other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = hashCode * 397 ^ g.GetHashCode(); + hashCode = hashCode * 397 ^ b.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgbFloat left, ColorRgbFloat right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgbFloat left, ColorRgbFloat right) + { + return !left.Equals(right); + } + + public static ColorRgbFloat operator +(ColorRgbFloat left, ColorRgbFloat right) + { + return new ColorRgbFloat( + left.r + right.r, + left.g + right.g, + left.b + right.b); + } + + public static ColorRgbFloat operator -(ColorRgbFloat left, ColorRgbFloat right) + { + return new ColorRgbFloat( + left.r - right.r, + left.g - right.g, + left.b - right.b); + } + + public static ColorRgbFloat operator /(ColorRgbFloat left, float right) + { + return new ColorRgbFloat( + left.r / right, + left.g / right, + left.b / right + ); + } + + public static ColorRgbFloat operator *(ColorRgbFloat left, float right) + { + return new ColorRgbFloat( + left.r * right, + left.g * right, + left.b * right + ); + } + + public static ColorRgbFloat operator *(float left, ColorRgbFloat right) + { + return new ColorRgbFloat( + right.r * left, + right.g * left, + right.b * left + ); + } + + public override string ToString() + { + return $"r : {r:0.00} g : {g:0.00} b : {b:0.00}"; + } + + public ColorRgba32 ToRgba32() + { + return new ColorRgba32( + ByteHelper.ClampToByte(r * 255), + ByteHelper.ClampToByte(g * 255), + ByteHelper.ClampToByte(b * 255), + 255 + ); + } + + public Vector3 ToVector3() + { + return new Vector3(r, g, b); + } + + internal float CalcLogDist(ColorRgbFloat other) + { + var dr = Math.Sign(other.r) * MathF.Log(1 + MathF.Abs(other.r)) - Math.Sign(r) * MathF.Log(1 + MathF.Abs(r)); + var dg = Math.Sign(other.g) * MathF.Log(1 + MathF.Abs(other.g)) - Math.Sign(g) * MathF.Log(1 + MathF.Abs(g)); + var db = Math.Sign(other.b) * MathF.Log(1 + MathF.Abs(other.b)) - Math.Sign(b) * MathF.Log(1 + MathF.Abs(b)); + return MathF.Sqrt(dr * dr + dg * dg + db * db); + } + + internal float CalcDist(ColorRgbFloat other) + { + var dr = other.r - r; + var dg = other.g - g; + var db = other.b - b; + return MathF.Sqrt(dr * dr + dg * dg + db * db); + } + + internal void ClampToPositive() + { + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + } + + internal void ClampToHalf() + { + if (r < Half.MinValue) r = Half.MinValue; + else if (g > Half.MaxValue) g = Half.MaxValue; + if (b < Half.MinValue) b = Half.MinValue; + else if (r > Half.MaxValue) r = Half.MaxValue; + if (g < Half.MinValue) g = Half.MinValue; + else if (b > Half.MaxValue) b = Half.MaxValue; + } + } + + internal struct ColorYCbCr + { + public float y; + public float cb; + public float cr; + + public ColorYCbCr(float y, float cb, float cr) + { + this.y = y; + this.cb = cb; + this.cr = cr; + } + + internal ColorYCbCr(ColorRgb24 rgb) + { + var fr = (float)rgb.r / 255; + var fg = (float)rgb.g / 255; + var fb = (float)rgb.b / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + internal ColorYCbCr(ColorRgbaFloat rgb) + { + var fr = rgb.r; + var fg = rgb.g; + var fb = rgb.b; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + internal ColorYCbCr(ColorRgbFloat rgb) + { + var fr = rgb.r; + var fg = rgb.g; + var fb = rgb.b; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + internal ColorYCbCr(ColorRgb565 rgb) + { + var fr = (float)rgb.R / 255; + var fg = (float)rgb.G / 255; + var fb = (float)rgb.B / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + public ColorYCbCr(ColorRgba32 rgba) + { + var fr = (float)rgba.r / 255; + var fg = (float)rgba.g / 255; + var fb = (float)rgba.b / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + public ColorYCbCr(Vector3 vec) + { + var fr = vec.X; + var fg = vec.Y; + var fb = vec.Z; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + } + + public ColorRgb565 ToColorRgb565() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return new ColorRgb565((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + } + + public ColorRgba32 ToColorRgba32() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return new ColorRgba32((byte)(r * 255), (byte)(g * 255), (byte)(b * 255), 255); + } + + public override string ToString() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return $"r : {r * 255} g : {g * 255} b : {b * 255}"; + } + + public float CalcDistWeighted(ColorYCbCr other, float yWeight = 4) + { + var dy = (y - other.y) * (y - other.y) * yWeight; + var dcb = (cb - other.cb) * (cb - other.cb); + var dcr = (cr - other.cr) * (cr - other.cr); + + return MathF.Sqrt(dy + dcb + dcr); + } + + public static ColorYCbCr operator +(ColorYCbCr left, ColorYCbCr right) + { + return new ColorYCbCr( + left.y + right.y, + left.cb + right.cb, + left.cr + right.cr); + } + + public static ColorYCbCr operator /(ColorYCbCr left, float right) + { + return new ColorYCbCr( + left.y / right, + left.cb / right, + left.cr / right); + } + } + + internal struct ColorRgb555 : IEquatable + { + public bool Equals(ColorRgb555 other) + { + return data == other.data; + } + + public override bool Equals(object obj) + { + return obj is ColorRgb555 other && Equals(other); + } + + public override int GetHashCode() + { + return data.GetHashCode(); + } + + public static bool operator ==(ColorRgb555 left, ColorRgb555 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgb555 left, ColorRgb555 right) + { + return !left.Equals(right); + } + + private const ushort ModeMask = 0b1_00000_00000_00000; + private const int ModeShift = 15; + private const ushort RedMask = 0b0_11111_00000_00000; + private const int RedShift = 10; + private const ushort GreenMask = 0b0_00000_11111_00000; + private const int GreenShift = 5; + private const ushort BlueMask = 0b0_00000_00000_11111; + + public ushort data; + + public byte Mode + { + readonly get + { + var mode = (data & ModeMask) >> ModeShift; + return (byte)mode; + } + set + { + var mode = value; + data = (ushort)(data & ~ModeMask); + data = (ushort)(data | mode << ModeShift); + } + } + + public byte R + { + readonly get + { + var r5 = (data & RedMask) >> RedShift; + return (byte)(r5 << 3 | r5 >> 2); + } + set + { + var r5 = value >> 3; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | r5 << RedShift); + } + } + + public byte G + { + readonly get + { + var g5 = (data & GreenMask) >> GreenShift; + return (byte)(g5 << 3 | g5 >> 2); + } + set + { + var g5 = value >> 3; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | g5 << GreenShift); + } + } + + public byte B + { + readonly get + { + var b5 = data & BlueMask; + return (byte)(b5 << 3 | b5 >> 2); + } + set + { + var b5 = value >> 3; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | b5); + } + } + + public int RawR + { + readonly get => (data & RedMask) >> RedShift; + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | value << RedShift); + } + } + + public int RawG + { + readonly get => (data & GreenMask) >> GreenShift; + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | value << GreenShift); + } + } + + public int RawB + { + readonly get => data & BlueMask; + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | value); + } + } + + public ColorRgb555(byte r, byte g, byte b) + { + data = 0; + R = r; + G = g; + B = b; + } + + public ColorRgb555(Vector3 colorVector) + { + data = 0; + R = ByteHelper.ClampToByte(colorVector.X * 255); + G = ByteHelper.ClampToByte(colorVector.Y * 255); + B = ByteHelper.ClampToByte(colorVector.Z * 255); + } + + public ColorRgb555(ColorRgb24 color) + { + data = 0; + R = color.r; + G = color.g; + B = color.b; + } + + public readonly ColorRgb24 ToColorRgb24() + { + return new ColorRgb24(R, G, B); + } + + public override string ToString() + { + return $"r : {R} g : {G} b : {B}"; + } + + public ColorRgba32 ToColorRgba32() + { + return new ColorRgba32(R, G, B, 255); + } + } + + internal struct ColorRgb565 : IEquatable + { + public bool Equals(ColorRgb565 other) + { + return data == other.data; + } + + public override bool Equals(object obj) + { + return obj is ColorRgb565 other && Equals(other); + } + + public override int GetHashCode() + { + return data.GetHashCode(); + } + + public static bool operator ==(ColorRgb565 left, ColorRgb565 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgb565 left, ColorRgb565 right) + { + return !left.Equals(right); + } + + private const ushort RedMask = 0b11111_000000_00000; + private const int RedShift = 11; + private const ushort GreenMask = 0b00000_111111_00000; + private const int GreenShift = 5; + private const ushort BlueMask = 0b00000_000000_11111; + + public ushort data; + + public byte R + { + readonly get + { + var r5 = (data & RedMask) >> RedShift; + return (byte)(r5 << 3 | r5 >> 2); + } + set + { + var r5 = value >> 3; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | r5 << RedShift); + } + } + + public byte G + { + readonly get + { + var g6 = (data & GreenMask) >> GreenShift; + return (byte)(g6 << 2 | g6 >> 4); + } + set + { + var g6 = value >> 2; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | g6 << GreenShift); + } + } + + public byte B + { + readonly get + { + var b5 = data & BlueMask; + return (byte)(b5 << 3 | b5 >> 2); + } + set + { + var b5 = value >> 3; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | b5); + } + } + + public int RawR + { + readonly get { return (data & RedMask) >> RedShift; } + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~RedMask); + data = (ushort)(data | value << RedShift); + } + } + + public int RawG + { + readonly get { return (data & GreenMask) >> GreenShift; } + set + { + if (value > 63) value = 63; + if (value < 0) value = 0; + data = (ushort)(data & ~GreenMask); + data = (ushort)(data | value << GreenShift); + } + } + + public int RawB + { + readonly get { return data & BlueMask; } + set + { + if (value > 31) value = 31; + if (value < 0) value = 0; + data = (ushort)(data & ~BlueMask); + data = (ushort)(data | value); + } + } + + public ColorRgb565(byte r, byte g, byte b) + { + data = 0; + R = r; + G = g; + B = b; + } + + public ColorRgb565(Vector3 colorVector) + { + data = 0; + R = ByteHelper.ClampToByte(colorVector.X * 255); + G = ByteHelper.ClampToByte(colorVector.Y * 255); + B = ByteHelper.ClampToByte(colorVector.Z * 255); + } + + public ColorRgb565(ColorRgb24 color) + { + data = 0; + R = color.r; + G = color.g; + B = color.b; + } + + public readonly ColorRgb24 ToColorRgb24() + { + return new ColorRgb24(R, G, B); + } + + public override string ToString() + { + return $"r : {R} g : {G} b : {B}"; + } + + public ColorRgba32 ToColorRgba32() + { + return new ColorRgba32(R, G, B, 255); + } + } + + internal struct ColorRgb24 : IEquatable + { + public byte r, g, b; + + public ColorRgb24(byte r, byte g, byte b) + { + this.r = r; + this.g = g; + this.b = b; + } + + public ColorRgb24(ColorRgb565 color) + { + r = color.R; + g = color.G; + b = color.B; + } + + public ColorRgb24(ColorRgba32 color) + { + r = color.r; + g = color.g; + b = color.b; + } + + public bool Equals(ColorRgb24 other) + { + return r == other.r && g == other.g && b == other.b; + } + + public override bool Equals(object obj) + { + return obj is ColorRgb24 other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = hashCode * 397 ^ g.GetHashCode(); + hashCode = hashCode * 397 ^ b.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgb24 left, ColorRgb24 right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgb24 left, ColorRgb24 right) + { + return !left.Equals(right); + } + + public static ColorRgb24 operator +(ColorRgb24 left, ColorRgb24 right) + { + return new ColorRgb24( + ByteHelper.ClampToByte(left.r + right.r), + ByteHelper.ClampToByte(left.g + right.g), + ByteHelper.ClampToByte(left.b + right.b)); + } + + public static ColorRgb24 operator -(ColorRgb24 left, ColorRgb24 right) + { + return new ColorRgb24( + ByteHelper.ClampToByte(left.r - right.r), + ByteHelper.ClampToByte(left.g - right.g), + ByteHelper.ClampToByte(left.b - right.b)); + } + + public static ColorRgb24 operator /(ColorRgb24 left, double right) + { + return new ColorRgb24( + ByteHelper.ClampToByte((int)(left.r / right)), + ByteHelper.ClampToByte((int)(left.g / right)), + ByteHelper.ClampToByte((int)(left.b / right)) + ); + } + + public static ColorRgb24 operator *(ColorRgb24 left, double right) + { + return new ColorRgb24( + ByteHelper.ClampToByte((int)(left.r * right)), + ByteHelper.ClampToByte((int)(left.g * right)), + ByteHelper.ClampToByte((int)(left.b * right)) + ); + } + + public override string ToString() + { + return $"r : {r} g : {g} b : {b}"; + } + } + + internal struct ColorYCbCrAlpha + { + public float y; + public float cb; + public float cr; + public float alpha; + + public ColorYCbCrAlpha(float y, float cb, float cr, float alpha) + { + this.y = y; + this.cb = cb; + this.cr = cr; + this.alpha = alpha; + } + + public ColorYCbCrAlpha(ColorRgb24 rgb) + { + var fr = (float)rgb.r / 255; + var fg = (float)rgb.g / 255; + var fb = (float)rgb.b / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = 1; + } + + public ColorYCbCrAlpha(ColorRgb565 rgb) + { + var fr = (float)rgb.R / 255; + var fg = (float)rgb.G / 255; + var fb = (float)rgb.B / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = 1; + } + + public ColorYCbCrAlpha(ColorRgba32 rgba) + { + var fr = (float)rgba.r / 255; + var fg = (float)rgba.g / 255; + var fb = (float)rgba.b / 255; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = rgba.a / 255f; + } + + public ColorYCbCrAlpha(ColorRgbaFloat rgba) + { + var fr = rgba.r; + var fg = rgba.g; + var fb = rgba.b; + + y = 0.2989f * fr + 0.5866f * fg + 0.1145f * fb; + cb = -0.1687f * fr - 0.3313f * fg + 0.5000f * fb; + cr = 0.5000f * fr - 0.4184f * fg - 0.0816f * fb; + alpha = rgba.a; + } + + + public ColorRgb565 ToColorRgb565() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return new ColorRgb565((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + } + + public override string ToString() + { + var r = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 0.0000 * cb + 1.4022 * cr))); + var g = Math.Max(0.0f, Math.Min(1.0f, (float)(y - 0.3456 * cb - 0.7145 * cr))); + var b = Math.Max(0.0f, Math.Min(1.0f, (float)(y + 1.7710 * cb + 0.0000 * cr))); + + return $"r : {r * 255} g : {g * 255} b : {b * 255}"; + } + + public float CalcDistWeighted(ColorYCbCrAlpha other, float yWeight = 4, float aWeight = 1) + { + var dy = (y - other.y) * (y - other.y) * yWeight; + var dcb = (cb - other.cb) * (cb - other.cb); + var dcr = (cr - other.cr) * (cr - other.cr); + var da = (alpha - other.alpha) * (alpha - other.alpha) * aWeight; + + return MathF.Sqrt(dy + dcb + dcr + da); + } + + public static ColorYCbCrAlpha operator +(ColorYCbCrAlpha left, ColorYCbCrAlpha right) + { + return new ColorYCbCrAlpha( + left.y + right.y, + left.cb + right.cb, + left.cr + right.cr, + left.alpha + right.alpha); + } + + public static ColorYCbCrAlpha operator /(ColorYCbCrAlpha left, float right) + { + return new ColorYCbCrAlpha( + left.y / right, + left.cb / right, + left.cr / right, + left.alpha / right); + } + } + + internal struct ColorXyz + { + public float x; + public float y; + public float z; + + public ColorXyz(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public ColorXyz(ColorRgb24 color) + { + this = ColorToXyz(color); + } + + public ColorXyz(ColorRgbFloat color) + { + this = ColorToXyz(color); + } + + public ColorRgbFloat ToColorRgbFloat() + { + return new ColorRgbFloat( + 3.2404542f * x - 1.5371385f * y - 0.4985314f * z, + -0.9692660f * x + 1.8760108f * y + 0.0415560f * z, + 0.0556434f * x - 0.2040259f * y + 1.0572252f * z + ); + } + + public static ColorXyz ColorToXyz(ColorRgb24 color) + { + var r = PivotRgb(color.r / 255.0f); + var g = PivotRgb(color.g / 255.0f); + var b = PivotRgb(color.b / 255.0f); + + // Observer. = 2°, Illuminant = D65 + return new ColorXyz(r * 0.4124f + g * 0.3576f + b * 0.1805f, r * 0.2126f + g * 0.7152f + b * 0.0722f, r * 0.0193f + g * 0.1192f + b * 0.9505f); + } + + public static ColorXyz ColorToXyz(ColorRgbFloat color) + { + var r = PivotRgb(color.r); + var g = PivotRgb(color.g); + var b = PivotRgb(color.b); + + // Observer. = 2°, Illuminant = D65 + return new ColorXyz(r * 0.4124f + g * 0.3576f + b * 0.1805f, r * 0.2126f + g * 0.7152f + b * 0.0722f, r * 0.0193f + g * 0.1192f + b * 0.9505f); + } + + private static float PivotRgb(float n) + { + return (n > 0.04045f ? MathF.Pow((n + 0.055f) / 1.055f, 2.4f) : n / 12.92f) * 100; + } + } + + internal struct ColorLab + { + public float l; + public float a; + public float b; + + public ColorLab(float l, float a, float b) + { + this.l = l; + this.a = a; + this.b = b; + } + + public ColorLab(ColorRgb24 color) + { + this = ColorToLab(color); + } + + public ColorLab(ColorRgba32 color) + { + this = ColorToLab(new ColorRgb24(color.r, color.g, color.b)); + } + + public ColorLab(ColorRgbFloat color) + { + this = XyzToLab(new ColorXyz(color)); + } + + public static ColorLab ColorToLab(ColorRgb24 color) + { + var xyz = new ColorXyz(color); + return XyzToLab(xyz); + } + + + public static ColorLab XyzToLab(ColorXyz xyz) + { + var refX = 95.047f; // Observer= 2°, Illuminant= D65 + var refY = 100.000f; + var refZ = 108.883f; + + var x = PivotXyz(xyz.x / refX); + var y = PivotXyz(xyz.y / refY); + var z = PivotXyz(xyz.z / refZ); + + return new ColorLab(116 * y - 16, 500 * (x - y), 200 * (y - z)); + } + + private static float PivotXyz(float n) + { + var i = MathF.Cbrt(n); + return n > 0.008856f ? i : 7.787f * n + 16 / 116f; + } + } + + internal struct ColorRgbe : IEquatable + { + public byte r; + public byte g; + public byte b; + public byte e; + + public ColorRgbe(byte r, byte g, byte b, byte e) + { + this.r = r; + this.g = g; + this.b = b; + this.e = e; + } + + public ColorRgbe(ColorRgbFloat color) + { + var max = MathF.Max(color.b, MathF.Max(color.g, color.r)); + if (max <= 1e-32f) + { + r = g = b = e = 0; + } + else + { + MathHelper.FrExp(max, out var exponent); + var scale = MathHelper.LdExp(1f, -exponent + 8); + r = (byte)(scale * color.r); + g = (byte)(scale * color.g); + b = (byte)(scale * color.b); + e = (byte)(exponent + 128); + } + } + + public ColorRgbFloat ToColorRgbFloat(float exposure = 1.0f) + { + if (e == 0) + { + return new ColorRgbFloat(0, 0, 0); + } + else + { + var fexp = MathHelper.LdExp(1f, e - (128 + 8)) / exposure; + + return new ColorRgbFloat( + (r + 0.5f) * fexp, + (g + 0.5f) * fexp, + (b + 0.5f) * fexp + ); + } + } + + + public bool Equals(ColorRgbe other) + { + return r == other.r && g == other.g && b == other.b && e == other.e; + } + + public override bool Equals(object obj) + { + return obj is ColorRgbe other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = r.GetHashCode(); + hashCode = hashCode * 397 ^ g.GetHashCode(); + hashCode = hashCode * 397 ^ b.GetHashCode(); + hashCode = hashCode * 397 ^ e.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColorRgbe left, ColorRgbe right) + { + return left.Equals(right); + } + + public static bool operator !=(ColorRgbe left, ColorRgbe right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return $"{nameof(r)}: {r}, {nameof(g)}: {g}, {nameof(b)}: {b}, {nameof(e)}: {e}"; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ComponentHelper.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ComponentHelper.cs new file mode 100644 index 0000000..1c25185 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ComponentHelper.cs @@ -0,0 +1,84 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class ComponentHelper + { + public static ColorRgba32 ComponentToColor(ColorComponent component, byte componentValue) + { + switch (component) + { + case ColorComponent.R: + return new ColorRgba32(componentValue, 0, 0, 255); + + case ColorComponent.G: + return new ColorRgba32(0, componentValue, 0, 255); + + case ColorComponent.B: + return new ColorRgba32(0, 0, componentValue, 255); + + case ColorComponent.A: + return new ColorRgba32(0, 0, 0, componentValue); + + case ColorComponent.Luminance: + return new ColorRgba32(componentValue, componentValue, componentValue, 255); + + default: + throw new InvalidOperationException("Unsupported component."); + } + } + + public static ColorRgba32 ComponentToColor(ColorRgba32 existingColor, ColorComponent component, byte componentValue) + { + switch (component) + { + case ColorComponent.R: + existingColor.r = componentValue; + break; + + case ColorComponent.G: + existingColor.g = componentValue; + break; + + case ColorComponent.B: + existingColor.b = componentValue; + break; + + case ColorComponent.A: + existingColor.a = componentValue; + break; + + case ColorComponent.Luminance: + existingColor.r = existingColor.g = existingColor.b = componentValue; + break; + + default: + throw new InvalidOperationException("Unsupported component."); + } + + return existingColor; + } + + public static byte ColorToComponent(ColorRgba32 color, ColorComponent component) + { + switch (component) + { + case ColorComponent.R: + return color.r; + + case ColorComponent.G: + return color.g; + + case ColorComponent.B: + return color.b; + + case ColorComponent.A: + return color.a; + + case ColorComponent.Luminance: + return (byte)(new ColorYCbCr(color).y * 255); + + default: + throw new InvalidOperationException("Unsupported component."); + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/CompressionFormat.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/CompressionFormat.cs new file mode 100644 index 0000000..a3af0bb --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/CompressionFormat.cs @@ -0,0 +1,110 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public enum CompressionFormat + { + /// + /// Raw unsigned byte 8-bit Luminance data + /// + R, + /// + /// Raw unsigned byte 16-bit RG data + /// + Rg, + /// + /// Raw unsigned byte 24-bit RGB data + /// + Rgb, + /// + /// Raw unsigned byte 32-bit RGBA data + /// + Rgba, + /// + /// Raw unsigned byte 32-bit BGRA data + /// + Bgra, + /// + /// BC1 / DXT1 with no alpha. Very widely supported and good compression ratio. + /// + Bc1, + /// + /// BC1 / DXT1 with 1-bit of alpha. + /// + Bc1WithAlpha, + /// + /// BC2 / DXT3 encoding with alpha. Good for sharp alpha transitions. + /// + Bc2, + /// + /// BC3 / DXT5 encoding with alpha. Good for smooth alpha transitions. + /// + Bc3, + /// + /// BC4 single-channel encoding. Only luminance is encoded. + /// + Bc4, + /// + /// BC5 dual-channel encoding. Only red and green channels are encoded. + /// + Bc5, + /// + /// BC6H / BPTC unsigned float encoding. Can compress HDR textures without alpha. Does not support negative values. + /// + Bc6U, + /// + /// BC6H / BPTC signed float encoding. Can compress HDR textures without alpha. Supports negative values. + /// + Bc6S, + /// + /// BC7 / BPTC unorm encoding. Very high Quality rgba or rgb encoding. Also very slow. + /// + Bc7, + /// + /// ATC / Adreno Texture Compression encoding. Derivative of BC1. + /// + Atc, + /// + /// ATC / Adreno Texture Compression encoding. Derivative of BC2. Good for sharp alpha transitions. + /// + AtcExplicitAlpha, + /// + /// ATC / Adreno Texture Compression encoding. Derivative of BC3. Good for smooth alpha transitions. + /// + AtcInterpolatedAlpha, + /// + /// Unknown format + /// + Unknown + } + + public static class CompressionFormatExtensions + { + public static bool IsCompressedFormat(this CompressionFormat format) + { + switch (format) + { + case CompressionFormat.R: + case CompressionFormat.Rg: + case CompressionFormat.Rgb: + case CompressionFormat.Rgba: + case CompressionFormat.Bgra: + return false; + + default: + return true; + } + } + + public static bool IsHdrFormat(this CompressionFormat format) + { + switch (format) + { + case CompressionFormat.Bc6S: + case CompressionFormat.Bc6U: + return true; + + default: + return false; + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/EncodedBlocks.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/EncodedBlocks.cs new file mode 100644 index 0000000..586eecd --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/EncodedBlocks.cs @@ -0,0 +1,455 @@ +using System.Runtime.InteropServices; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + [StructLayout(LayoutKind.Sequential)] + internal struct Bc1Block + { + public ColorRgb565 color0; + public ColorRgb565 color1; + public uint colorIndices; + + public int this[int index] + { + readonly get => (int)(colorIndices >> index * 2) & 0b11; + set + { + colorIndices = (uint)(colorIndices & ~(0b11 << index * 2)); + var val = value & 0b11; + colorIndices = colorIndices | (uint)val << index * 2; + } + } + + public readonly bool HasAlphaOrBlack => color0.data <= color1.data; + + public readonly RawBlock4X4Rgba32 Decode(bool useAlpha) + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var color0 = this.color0.ToColorRgb24(); + var color1 = this.color1.ToColorRgb24(); + + useAlpha = useAlpha && HasAlphaOrBlack; + + var colors = HasAlphaOrBlack ? + stackalloc ColorRgb24[] { + color0, + color1, + color0.InterpolateHalf(color1), + new ColorRgb24(0, 0, 0) + } : stackalloc ColorRgb24[] { + color0, + color1, + color0.InterpolateThird(color1, 1), + color0.InterpolateThird(color1, 2) + }; + + for (var i = 0; i < pixels.Length; i++) + { + var colorIndex = (int)(colorIndices >> i * 2 & 0b11); + var color = colors[colorIndex]; + if (useAlpha && colorIndex == 3) + { + pixels[i] = new ColorRgba32(0, 0, 0, 0); + } + else + { + pixels[i] = new ColorRgba32(color.r, color.g, color.b, 255); + } + } + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc2Block + { + public Bc2AlphaBlock alphaBlock; + public ColorRgb565 color0; + public ColorRgb565 color1; + public uint colorIndices; + + public int this[int index] + { + readonly get => (int)(colorIndices >> index * 2) & 0b11; + set + { + colorIndices = (uint)(colorIndices & ~(0b11 << index * 2)); + var val = value & 0b11; + colorIndices = colorIndices | (uint)val << index * 2; + } + } + + public readonly byte GetAlpha(int index) => alphaBlock.GetAlpha(index); + + public void SetAlpha(int index, byte alpha) => alphaBlock.SetAlpha(index, alpha); + + public readonly RawBlock4X4Rgba32 Decode() + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var color0 = this.color0.ToColorRgb24(); + var color1 = this.color1.ToColorRgb24(); + + Span colors = stackalloc ColorRgb24[] { + color0, + color1, + color0.InterpolateThird(color1, 1), + color0.InterpolateThird(color1, 2) + }; + + for (var i = 0; i < pixels.Length; i++) + { + var colorIndex = (int)(colorIndices >> i * 2 & 0b11); + var color = colors[colorIndex]; + + pixels[i] = new ColorRgba32(color.r, color.g, color.b, GetAlpha(i)); + } + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc3Block + { + public Bc4ComponentBlock alphaBlock; + public ColorRgb565 color0; + public ColorRgb565 color1; + public uint colorIndices; + + public int this[int index] + { + readonly get => (int)(colorIndices >> index * 2) & 0b11; + set + { + colorIndices = (uint)(colorIndices & ~(0b11 << index * 2)); + var val = value & 0b11; + colorIndices = colorIndices | (uint)val << index * 2; + } + } + + public byte Alpha0 + { + get => alphaBlock.Endpoint0; + set => alphaBlock.Endpoint0 = value; + } + + public byte Alpha1 + { + get => alphaBlock.Endpoint1; + set => alphaBlock.Endpoint1 = value; + } + + public readonly byte GetAlphaIndex(int pixelIndex) => alphaBlock.GetComponentIndex(pixelIndex); + + public void SetAlphaIndex(int pixelIndex, byte alphaIndex) => alphaBlock.SetComponentIndex(pixelIndex, alphaIndex); + + public readonly RawBlock4X4Rgba32 Decode() + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var color0 = this.color0.ToColorRgb24(); + var color1 = this.color1.ToColorRgb24(); + + Span colors = stackalloc ColorRgb24[] { + color0, + color1, + color0.InterpolateThird(color1, 1), + color0.InterpolateThird(color1, 2) + }; + + var alphas = alphaBlock.Decode(); + + for (var i = 0; i < pixels.Length; i++) + { + var colorIndex = (int)(colorIndices >> i * 2 & 0b11); + var color = colors[colorIndex]; + + pixels[i] = new ColorRgba32(color.r, color.g, color.b, alphas[i]); + } + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc4Block + { + public Bc4ComponentBlock componentBlock; + + public byte Endpoint0 + { + readonly get => componentBlock.Endpoint0; + set => componentBlock.Endpoint0 = value; + } + + public byte Endpoint1 + { + readonly get => componentBlock.Endpoint1; + set => componentBlock.Endpoint1 = value; + } + + public readonly byte GetComponentIndex(int pixelIndex) => componentBlock.GetComponentIndex(pixelIndex); + + public void SetComponentIndex(int pixelIndex, byte redIndex) => componentBlock.SetComponentIndex(pixelIndex, redIndex); + + public readonly RawBlock4X4Rgba32 Decode(ColorComponent component = ColorComponent.R) + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var components = componentBlock.Decode(); + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i] = ComponentHelper.ComponentToColor(component, components[i]); + } + + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc5Block + { + public Bc4ComponentBlock redBlock; + public Bc4ComponentBlock greenBlock; + + public byte Red0 + { + readonly get => redBlock.Endpoint0; + set => redBlock.Endpoint0 = value; + } + + public byte Red1 + { + readonly get => redBlock.Endpoint1; + set => redBlock.Endpoint1 = value; + } + + public byte Green0 + { + readonly get => greenBlock.Endpoint0; + set => greenBlock.Endpoint0 = value; + } + + public byte Green1 + { + readonly get => greenBlock.Endpoint1; + set => greenBlock.Endpoint1 = value; + } + + public readonly byte GetRedIndex(int pixelIndex) => redBlock.GetComponentIndex(pixelIndex); + + public void SetRedIndex(int pixelIndex, byte redIndex) => redBlock.SetComponentIndex(pixelIndex, redIndex); + + public readonly byte GetGreenIndex(int pixelIndex) => greenBlock.GetComponentIndex(pixelIndex); + + public void SetGreenIndex(int pixelIndex, byte greenIndex) => greenBlock.SetComponentIndex(pixelIndex, greenIndex); + + public readonly RawBlock4X4Rgba32 Decode(ColorComponent component1 = ColorComponent.R, ColorComponent component2 = ColorComponent.G) + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var reds = redBlock.Decode(); + var greens = greenBlock.Decode(); + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i] = ComponentHelper.ComponentToColor(component1, reds[i]); + pixels[i] = ComponentHelper.ComponentToColor(pixels[i], component2, greens[i]); + } + + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AtcBlock + { + public ColorRgb555 color0; + public ColorRgb565 color1; + public uint colorIndices; + + public int this[int index] + { + readonly get => (int)(colorIndices >> index * 2) & 0b11; + set + { + colorIndices = (uint)(colorIndices & ~(0b11 << index * 2)); + var val = value & 0b11; + colorIndices = colorIndices | (uint)val << index * 2; + } + } + + public readonly RawBlock4X4Rgba32 Decode() + { + var output = new RawBlock4X4Rgba32(); + var pixels = output.AsSpan; + + var color0 = this.color0.ToColorRgb24(); + var color1 = this.color1.ToColorRgb24(); + + Span colors = stackalloc ColorRgb24[] { + new ColorRgb24(0, 0, 0), + color0.InterpolateFourthAtc(color1, 1), + color0, + color1 + }; + + for (var i = 0; i < pixels.Length; i++) + { + var colorIndex = this[i]; + + var color = this.color0.Mode == 0 ? color0.InterpolateThird(color1, colorIndex) : colors[colorIndex]; + + pixels[i] = new ColorRgba32(color.r, color.g, color.b, 255); + } + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc2AlphaBlock + { + public ulong alphas; + + public readonly byte GetAlpha(int index) + { + var mask = 0xFUL << index * 4; + var shift = index * 4; + var alphaUnscaled = (alphas & mask) >> shift; + return (byte)(alphaUnscaled * 17); + } + + public void SetAlpha(int index, byte alpha) + { + var mask = 0xFUL << index * 4; + var shift = index * 4; + alphas &= ~mask; + var a = (byte)(alpha / 17); + alphas |= (ulong)(a & 0xF) << shift; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct Bc4ComponentBlock + { + public ulong componentBlock; + + public byte Endpoint0 + { + readonly get => (byte)(componentBlock & 0xFFUL); + set + { + componentBlock &= ~0xFFUL; + componentBlock |= value; + } + } + + public byte Endpoint1 + { + readonly get => (byte)(componentBlock >> 8 & 0xFFUL); + set + { + componentBlock &= ~0xFF00UL; + componentBlock |= (ulong)value << 8; + } + } + + public readonly byte GetComponentIndex(int pixelIndex) + { + var mask = 0b0111UL << pixelIndex * 3 + 16; + var shift = pixelIndex * 3 + 16; + var redIndex = (componentBlock & mask) >> shift; + return (byte)redIndex; + } + + public void SetComponentIndex(int pixelIndex, byte redIndex) + { + var mask = 0b0111UL << pixelIndex * 3 + 16; + var shift = pixelIndex * 3 + 16; + componentBlock &= ~mask; + componentBlock |= (ulong)(redIndex & 0b111) << shift; + } + + public readonly byte[] Decode() + { + var output = new byte[16]; + + var c0 = Endpoint0; + var c1 = Endpoint1; + + var components = c0 > c1 ? stackalloc byte[] { + c0, + c1, + c0.InterpolateSeventh(c1, 1), + c0.InterpolateSeventh(c1, 2), + c0.InterpolateSeventh(c1, 3), + c0.InterpolateSeventh(c1, 4), + c0.InterpolateSeventh(c1, 5), + c0.InterpolateSeventh(c1, 6), + } : stackalloc byte[] { + c0, + c1, + c0.InterpolateFifth(c1, 1), + c0.InterpolateFifth(c1, 2), + c0.InterpolateFifth(c1, 3), + c0.InterpolateFifth(c1, 4), + 0, + 255 + }; + + for (var i = 0; i < output.Length; i++) + { + var index = GetComponentIndex(i); + output[i] = components[index]; + } + + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AtcExplicitAlphaBlock + { + public Bc2AlphaBlock alphas; + public AtcBlock colors; + + public readonly RawBlock4X4Rgba32 Decode() + { + var output = colors.Decode(); + var pixels = output.AsSpan; + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i].a = alphas.GetAlpha(i); + } + return output; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct AtcInterpolatedAlphaBlock + { + public Bc4ComponentBlock alphas; + public AtcBlock colors; + + public readonly RawBlock4X4Rgba32 Decode() + { + var output = colors.Decode(); + var pixels = output.AsSpan; + + var componentValues = alphas.Decode(); + + for (var i = 0; i < pixels.Length; i++) + { + pixels[i].a = componentValues[i]; + } + return output; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/GLFormat.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/GLFormat.cs new file mode 100644 index 0000000..788f758 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/GLFormat.cs @@ -0,0 +1,199 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public enum GlFormat : uint + { + GlRed = 0x1903, + GlBgra = 0x80E1, + GlRgb = 0x1907, + GlRgba = 0x1908, + GlRg = 0x8227, + GlRedInteger = 0x8D94, + GlRgInteger = 0x8228, + GlRedSnorm = 0x8F90, + GlRgSnorm = 0x8F91, + GlRgbSnorm = 0x8F92, + GlRgbaSnorm = 0x8F93, + } + + public enum GlType : uint + { + GlByte = 5120, + GlUnsignedByte = 5121, + GlShort = 5122, + GlUnsignedShort = 5123, + GlInt = 5124, + GlUnsignedInt = 5125, + GlFloat = 5126, + GlHalfFloat = 5131, + GlUnsignedByte233Rev = 33634, + GlUnsignedByte332 = 32818, + GlUnsignedInt1010102 = 32822, + GlUnsignedInt2101010Rev = 33640, + GlUnsignedInt8888 = 32821, + GlUnsignedInt8888Rev = 33639, + GlUnsignedShort1555Rev = 33638, + GlUnsignedShort4444 = 32819, + GlUnsignedShort4444Rev = 33637, + GlUnsignedShort5551 = 32820, + GlUnsignedShort565 = 33635, + GlUnsignedShort565Rev = 33636 + + } + + public enum GlInternalFormat : uint + { + GlRgba4 = 0x8056, + GlRgb5 = 0x8050, + GlRgb565 = 0x8D62, + GlRgba8 = 0x8058, + GlRgb5A1 = 0x8057, + GlRgba16 = 0x805B, + GlDepthComponent16 = 0x81A5, + GlDepthComponent24 = 0x81A6, + GlDepthComponent32F = 36012, + GlStencilIndex8 = 36168, + GlDepth24Stencil8 = 0x88F0, + GlDepth32FStencil8 = 36013, + + GlR8 = 0x8229, + GlRg8 = 0x822B, + GlRg16 = 0x822C, + GlR16F = 0x822D, + GlR32F = 0x822E, + GlRg16F = 0x822F, + GlRg32F = 0x8230, + GlRgba32F = 0x8814, + GlRgba16F = 0x881A, + + GlR8Ui = 33330, + GlR8I = 33329, + GlR16 = 33322, + GlR16I = 33331, + GlR16Ui = 33332, + GlR32I = 33333, + GlR32Ui = 33334, + + + GlRg8I = 33335, + GlRg8Ui = 33336, + GlRg16I = 33337, + GlRg16Ui = 33338, + GlRg32I = 33339, + GlRg32Ui = 33340, + + GlRgb8 = 32849, + GlRgb8I = 36239, + GlRgb8Ui = 36221, + + GlRgba12 = 32858, + GlRgba2 = 32853, + GlRgba8I = 36238, + GlRgba8Ui = 36220, + + GlRgba16I = 36232, + GlRgba16Ui = 36214, + GlRgba32I = 36226, + GlRgba32Ui = 36208, + + + GlR8Snorm = 0x8F94, + GlRg8Snorm = 0x8F95, + GlRgb8Snorm = 0x8F96, + GlRgba8Snorm = 0x8F97, + GlR16Snorm = 0x8F98, + GlRg16Snorm = 0x8F99, + GlRgb16Snorm = 0x8F9A, + GlRgba16Snorm = 0x8F9B, + + GlRgb10A2 = 32857, + GlRgb10A2Ui = 36975, + + GlRgb16 = 32852, + GlRgb16F = 34843, + GlRgb16I = 36233, + GlRgb16Ui = 36215, + + GlRgb32F = 34837, + GlRgb32I = 36227, + GlRgb32Ui = 36209, + + //BC1 + GlCompressedRgbS3TcDxt1Ext = 0x83F0, + GlCompressedSrgbS3TcDxt1Ext = 0x8C4C, + GlCompressedRgbaS3TcDxt1Ext = 0x83F1, + GlCompressedSrgbAlphaS3TcDxt1Ext = 0x8C4D, + + //BC2 + GlCompressedRgbaS3TcDxt3Ext = 0x83F2, + GlCompressedSrgbAlphaS3TcDxt3Ext = 0x8C4E, + + //BC3 + GlCompressedRgbaS3TcDxt5Ext = 0x83F3, + GlCompressedSrgbAlphaS3TcDxt5Ext = 0x8C4F, + + //BC4 & BC5 + GlCompressedRedGreenRgtc2Ext = 36285, + GlCompressedRedRgtc1Ext = 36283, + GlCompressedSignedRedGreenRgtc2Ext = 36286, + GlCompressedSignedRedRgtc1Ext = 36284, + + //BC6 & BC7 + GlCompressedRgbBptcSignedFloatArb = 36494, + GlCompressedRgbBptcUnsignedFloatArb = 36495, + GlCompressedRgbaBptcUnormArb = 36492, + GlCompressedSrgbAlphaBptcUnormArb = 36493, + + GlCompressedRgbAtc = 0x8C92, + GlCompressedRgbaAtcExplicitAlpha = 0x8C93, + GlCompressedRgbaAtcInterpolatedAlpha = 0x87EE, + + // ETC1 & 2 + GlEtc1Rgb8Oes = 0x8D64, + + GlCompressedR11Eac = 0x9270, + GlCompressedSignedR11Eac = 0x9271, + GlCompressedRg11Eac = 0x9272, + GlCompressedSignedRg11Eac = 0x9273, + + GlCompressedRgb8Etc2 = 0x9274, + GlCompressedSrgb8Etc2 = 0x9275, + GlCompressedRgb8PunchthroughAlpha1Etc2 = 0x9276, + GlCompressedSrgb8PunchthroughAlpha1Etc2 = 0x9277, + GlCompressedRgba8Etc2Eac = 0x9278, + GlCompressedSrgb8Alpha8Etc2Eac = 0x9279, + + // Apple extension BGRA8 + GlBgra8Extension = 0x93A1, + + // ASTC + GlCompressedRgbaAstc4X4Khr = 0x93B0, + GlCompressedRgbaAstc5X4Khr = 0x93B1, + GlCompressedRgbaAstc5X5Khr = 0x93B2, + GlCompressedRgbaAstc6X5Khr = 0x93B3, + GlCompressedRgbaAstc6X6Khr = 0x93B4, + GlCompressedRgbaAstc8X5Khr = 0x93B5, + GlCompressedRgbaAstc8X6Khr = 0x93B6, + GlCompressedRgbaAstc8X8Khr = 0x93B7, + GlCompressedRgbaAstc10X5Khr = 0x93B8, + GlCompressedRgbaAstc10X6Khr = 0x93B9, + GlCompressedRgbaAstc10X8Khr = 0x93BA, + GlCompressedRgbaAstc10X10Khr = 0x93BB, + GlCompressedRgbaAstc12X10Khr = 0x93BC, + GlCompressedRgbaAstc12X12Khr = 0x93BD, + + GlCompressedSrgb8Alpha8Astc4X4Khr = 0x93D0, + GlCompressedSrgb8Alpha8Astc5X4Khr = 0x93D1, + GlCompressedSrgb8Alpha8Astc5X5Khr = 0x93D2, + GlCompressedSrgb8Alpha8Astc6X5Khr = 0x93D3, + GlCompressedSrgb8Alpha8Astc6X6Khr = 0x93D4, + GlCompressedSrgb8Alpha8Astc8X5Khr = 0x93D5, + GlCompressedSrgb8Alpha8Astc8X6Khr = 0x93D6, + GlCompressedSrgb8Alpha8Astc8X8Khr = 0x93D7, + GlCompressedSrgb8Alpha8Astc10X5Khr = 0x93D8, + GlCompressedSrgb8Alpha8Astc10X6Khr = 0x93D9, + GlCompressedSrgb8Alpha8Astc10X8Khr = 0x93DA, + GlCompressedSrgb8Alpha8Astc10X10Khr = 0x93DB, + GlCompressedSrgb8Alpha8Astc12X10Khr = 0x93DC, + GlCompressedSrgb8Alpha8Astc12X12Khr = 0x93DD + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Half.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Half.cs new file mode 100644 index 0000000..c67d62b --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Half.cs @@ -0,0 +1,1089 @@ +/// ================ Half.cs ==================== +/// The code is free to use for any reason without any restrictions. +/// Ladislav Lang (2009), Joannes Vermorel (2017) + +using System.Diagnostics; +using System.Globalization; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// Represents a half-precision floating point number. + /// + /// + /// Note: + /// Half is not fast enought and precision is also very bad, + /// so is should not be used for mathematical computation (use Single instead). + /// The main advantage of Half type is lower memory cost: two bytes per number. + /// Half is typically used in graphical applications. + /// + /// Note: + /// All functions, where is used conversion half->float/float->half, + /// are approx. ten times slower than float->double/double->float, i.e. ~3ns on 2GHz CPU. + /// + /// References: + /// - Code retrieved from http://sourceforge.net/p/csharp-half/code/HEAD/tree/ on 2015-12-04 + /// - Fast Half Float Conversions, Jeroen van der Zijp, link: http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf + /// - IEEE 754 revision, link: http://grouper.ieee.org/groups/754/ + /// + [Serializable] + public struct Half : IComparable, IFormattable, IConvertible, IComparable, IEquatable + { + /// + /// Internal representation of the half-precision floating-point number. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal ushort Value; + + #region Constants + /// + /// Represents the smallest positive System.Half value greater than zero. This field is constant. + /// + public static readonly Half Epsilon = ToHalf(0x0001); + /// + /// Represents the largest possible value of System.Half. This field is constant. + /// + public static readonly Half MaxValue = ToHalf(0x7bff); + /// + /// Represents the smallest possible value of System.Half. This field is constant. + /// + public static readonly Half MinValue = ToHalf(0xfbff); + /// + /// Represents not a number (NaN). This field is constant. + /// + public static readonly Half NaN = ToHalf(0xfe00); + /// + /// Represents negative infinity. This field is constant. + /// + public static readonly Half NegativeInfinity = ToHalf(0xfc00); + /// + /// Represents positive infinity. This field is constant. + /// + public static readonly Half PositiveInfinity = ToHalf(0x7c00); + #endregion + + #region Constructors + /// + /// Initializes a new instance of System.Half to the value of the specified single-precision floating-point number. + /// + /// The value to represent as a System.Half. + public Half(float value) { this = HalfHelper.SingleToHalf(value); } + /// + /// Initializes a new instance of System.Half to the value of the specified 32-bit signed integer. + /// + /// The value to represent as a System.Half. + public Half(int value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified 64-bit signed integer. + /// + /// The value to represent as a System.Half. + public Half(long value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified double-precision floating-point number. + /// + /// The value to represent as a System.Half. + public Half(double value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified decimal number. + /// + /// The value to represent as a System.Half. + public Half(decimal value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified 32-bit unsigned integer. + /// + /// The value to represent as a System.Half. + public Half(uint value) : this((float)value) { } + /// + /// Initializes a new instance of System.Half to the value of the specified 64-bit unsigned integer. + /// + /// The value to represent as a System.Half. + public Half(ulong value) : this((float)value) { } + #endregion + + #region Numeric operators + + /// + /// Returns the result of multiplying the specified System.Half value by negative one. + /// + /// A System.Half. + /// A System.Half with the value of half, but the opposite sign. -or- Zero, if half is zero. + public static Half Negate(Half half) { return -half; } + /// + /// Adds two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// A System.Half value that is the sum of half1 and half2. + public static Half Add(Half half1, Half half2) { return half1 + half2; } + /// + /// Subtracts one specified System.Half value from another. + /// + /// A System.Half (the minuend). + /// A System.Half (the subtrahend). + /// The System.Half result of subtracting half2 from half1. + public static Half Subtract(Half half1, Half half2) { return half1 - half2; } + /// + /// Multiplies two specified System.Half values. + /// + /// A System.Half (the multiplicand). + /// A System.Half (the multiplier). + /// A System.Half that is the result of multiplying half1 and half2. + public static Half Multiply(Half half1, Half half2) { return half1 * half2; } + /// + /// Divides two specified System.Half values. + /// + /// A System.Half (the dividend). + /// A System.Half (the divisor). + /// The System.Half that is the result of dividing half1 by half2. + /// half2 is zero. + public static Half Divide(Half half1, Half half2) { return half1 / half2; } + + /// + /// Returns the value of the System.Half operand (the sign of the operand is unchanged). + /// + /// The System.Half operand. + /// The value of the operand, half. + public static Half operator +(Half half) { return half; } + /// + /// Negates the value of the specified System.Half operand. + /// + /// The System.Half operand. + /// The result of half multiplied by negative one (-1). + public static Half operator -(Half half) { return HalfHelper.Negate(half); } + /// + /// Increments the System.Half operand by 1. + /// + /// The System.Half operand. + /// The value of half incremented by 1. + public static Half operator ++(Half half) { return (Half)(half + 1f); } + /// + /// Decrements the System.Half operand by one. + /// + /// The System.Half operand. + /// The value of half decremented by 1. + public static Half operator --(Half half) { return (Half)(half - 1f); } + /// + /// Adds two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of adding half1 and half2. + public static Half operator +(Half half1, Half half2) { return (Half)(half1 + (float)half2); } + /// + /// Subtracts two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of subtracting half1 and half2. + public static Half operator -(Half half1, Half half2) { return (Half)(half1 - (float)half2); } + /// + /// Multiplies two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of multiplying half1 by half2. + public static Half operator *(Half half1, Half half2) { return (Half)(half1 * (float)half2); } + /// + /// Divides two specified System.Half values. + /// + /// A System.Half (the dividend). + /// A System.Half (the divisor). + /// The System.Half result of half1 by half2. + public static Half operator /(Half half1, Half half2) { return (Half)(half1 / (float)half2); } + /// + /// Returns a value indicating whether two instances of System.Half are equal. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 and half2 are equal; otherwise, false. + public static bool operator ==(Half half1, Half half2) { return !IsNaN(half1) && half1.Value == half2.Value; } + /// + /// Returns a value indicating whether two instances of System.Half are not equal. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 and half2 are not equal; otherwise, false. + public static bool operator !=(Half half1, Half half2) { return half1.Value != half2.Value; } + /// + /// Returns a value indicating whether a specified System.Half is less than another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is less than half1; otherwise, false. + public static bool operator <(Half half1, Half half2) { return half1 < (float)half2; } + /// + /// Returns a value indicating whether a specified System.Half is greater than another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is greater than half2; otherwise, false. + public static bool operator >(Half half1, Half half2) { return half1 > (float)half2; } + /// + /// Returns a value indicating whether a specified System.Half is less than or equal to another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is less than or equal to half2; otherwise, false. + public static bool operator <=(Half half1, Half half2) { return half1 == half2 || half1 < half2; } + /// + /// Returns a value indicating whether a specified System.Half is greater than or equal to another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is greater than or equal to half2; otherwise, false. + public static bool operator >=(Half half1, Half half2) { return half1 == half2 || half1 > half2; } + #endregion + + #region Type casting operators + /// + /// Converts an 8-bit unsigned integer to a System.Half. + /// + /// An 8-bit unsigned integer. + /// A System.Half that represents the converted 8-bit unsigned integer. + public static implicit operator Half(byte value) { return new Half((float)value); } + /// + /// Converts a 16-bit signed integer to a System.Half. + /// + /// A 16-bit signed integer. + /// A System.Half that represents the converted 16-bit signed integer. + public static implicit operator Half(short value) { return new Half((float)value); } + /// + /// Converts a Unicode character to a System.Half. + /// + /// A Unicode character. + /// A System.Half that represents the converted Unicode character. + public static implicit operator Half(char value) { return new Half((float)value); } + /// + /// Converts a 32-bit signed integer to a System.Half. + /// + /// A 32-bit signed integer. + /// A System.Half that represents the converted 32-bit signed integer. + public static implicit operator Half(int value) { return new Half((float)value); } + /// + /// Converts a 64-bit signed integer to a System.Half. + /// + /// A 64-bit signed integer. + /// A System.Half that represents the converted 64-bit signed integer. + public static implicit operator Half(long value) { return new Half((float)value); } + /// + /// Converts a single-precision floating-point number to a System.Half. + /// + /// A single-precision floating-point number. + /// A System.Half that represents the converted single-precision floating point number. + public static explicit operator Half(float value) { return new Half(value); } + /// + /// Converts a double-precision floating-point number to a System.Half. + /// + /// A double-precision floating-point number. + /// A System.Half that represents the converted double-precision floating point number. + public static explicit operator Half(double value) { return new Half((float)value); } + /// + /// Converts a decimal number to a System.Half. + /// + /// decimal number + /// A System.Half that represents the converted decimal number. + public static explicit operator Half(decimal value) { return new Half((float)value); } + /// + /// Converts a System.Half to an 8-bit unsigned integer. + /// + /// A System.Half to convert. + /// An 8-bit unsigned integer that represents the converted System.Half. + public static explicit operator byte(Half value) { return (byte)(float)value; } + /// + /// Converts a System.Half to a Unicode character. + /// + /// A System.Half to convert. + /// A Unicode character that represents the converted System.Half. + public static explicit operator char(Half value) { return (char)(float)value; } + /// + /// Converts a System.Half to a 16-bit signed integer. + /// + /// A System.Half to convert. + /// A 16-bit signed integer that represents the converted System.Half. + public static explicit operator short(Half value) { return (short)(float)value; } + /// + /// Converts a System.Half to a 32-bit signed integer. + /// + /// A System.Half to convert. + /// A 32-bit signed integer that represents the converted System.Half. + public static explicit operator int(Half value) { return (int)(float)value; } + /// + /// Converts a System.Half to a 64-bit signed integer. + /// + /// A System.Half to convert. + /// A 64-bit signed integer that represents the converted System.Half. + public static explicit operator long(Half value) { return (long)(float)value; } + /// + /// Converts a System.Half to a single-precision floating-point number. + /// + /// A System.Half to convert. + /// A single-precision floating-point number that represents the converted System.Half. + public static implicit operator float(Half value) { return HalfHelper.HalfToSingle(value); } + /// + /// Converts a System.Half to a double-precision floating-point number. + /// + /// A System.Half to convert. + /// A double-precision floating-point number that represents the converted System.Half. + public static implicit operator double(Half value) { return (float)value; } + /// + /// Converts a System.Half to a decimal number. + /// + /// A System.Half to convert. + /// A decimal number that represents the converted System.Half. + public static explicit operator decimal(Half value) { return (decimal)(float)value; } + /// + /// Converts an 8-bit signed integer to a System.Half. + /// + /// An 8-bit signed integer. + /// A System.Half that represents the converted 8-bit signed integer. + public static implicit operator Half(sbyte value) { return new Half((float)value); } + /// + /// Converts a 16-bit unsigned integer to a System.Half. + /// + /// A 16-bit unsigned integer. + /// A System.Half that represents the converted 16-bit unsigned integer. + public static implicit operator Half(ushort value) { return new Half((float)value); } + /// + /// Converts a 32-bit unsigned integer to a System.Half. + /// + /// A 32-bit unsigned integer. + /// A System.Half that represents the converted 32-bit unsigned integer. + public static implicit operator Half(uint value) { return new Half((float)value); } + /// + /// Converts a 64-bit unsigned integer to a System.Half. + /// + /// A 64-bit unsigned integer. + /// A System.Half that represents the converted 64-bit unsigned integer. + public static implicit operator Half(ulong value) { return new Half((float)value); } + /// + /// Converts a System.Half to an 8-bit signed integer. + /// + /// A System.Half to convert. + /// An 8-bit signed integer that represents the converted System.Half. + public static explicit operator sbyte(Half value) { return (sbyte)(float)value; } + /// + /// Converts a System.Half to a 16-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 16-bit unsigned integer that represents the converted System.Half. + public static explicit operator ushort(Half value) { return (ushort)(float)value; } + /// + /// Converts a System.Half to a 32-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 32-bit unsigned integer that represents the converted System.Half. + public static explicit operator uint(Half value) { return (uint)(float)value; } + /// + /// Converts a System.Half to a 64-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 64-bit unsigned integer that represents the converted System.Half. + public static explicit operator ulong(Half value) { return (ulong)(float)value; } + #endregion + + /// + /// Compares this instance to a specified System.Half object. + /// + /// A System.Half object. + /// + /// A signed number indicating the relative values of this instance and value. + /// Return Value Meaning Less than zero This instance is less than value. Zero + /// This instance is equal to value. Greater than zero This instance is greater than value. + /// + public int CompareTo(Half other) + { + var result = 0; + if (this < other) + { + result = -1; + } + else if (this > other) + { + result = 1; + } + else if (this != other) + { + if (!IsNaN(this)) + { + result = 1; + } + else if (!IsNaN(other)) + { + result = -1; + } + } + + return result; + } + /// + /// Compares this instance to a specified System.Object. + /// + /// An System.Object or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Return Value Meaning Less than zero This instance is less than value. Zero + /// This instance is equal to value. Greater than zero This instance is greater + /// than value. -or- value is null. + /// + /// value is not a System.Half + public int CompareTo(object obj) + { + var result = 0; + if (obj == null) + { + result = 1; + } + else + { + if (obj is Half) + { + result = CompareTo((Half)obj); + } + else + { + throw new ArgumentException("Object must be of type Half."); + } + } + + return result; + } + /// + /// Returns a value indicating whether this instance and a specified System.Half object represent the same value. + /// + /// A System.Half object to compare to this instance. + /// true if value is equal to this instance; otherwise, false. + public bool Equals(Half other) + { + return other == this || IsNaN(other) && IsNaN(this); + } + /// + /// Returns a value indicating whether this instance and a specified System.Object + /// represent the same type and value. + /// + /// An System.Object. + /// true if value is a System.Half and equal to this instance; otherwise, false. + public override bool Equals(object obj) + { + var result = false; + if (obj is Half) + { + var half = (Half)obj; + if (half == this || IsNaN(half) && IsNaN(this)) + { + result = true; + } + } + + return result; + } + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return Value.GetHashCode(); + } + /// + /// Returns the System.TypeCode for value type System.Half. + /// + /// The enumerated constant (TypeCode)255. + public TypeCode GetTypeCode() + { + return (TypeCode)255; + } + + #region BitConverter & Math methods for Half + /// + /// Returns the specified half-precision floating point value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 2. + public static byte[] GetBytes(Half value) + { + return BitConverter.GetBytes(value.Value); + } + /// + /// Converts the value of a specified instance of System.Half to its equivalent binary representation. + /// + /// A System.Half value. + /// A 16-bit unsigned integer that contain the binary representation of value. + public static ushort GetBits(Half value) + { + return value.Value; + } + /// + /// Returns a half-precision floating point number converted from two bytes + /// at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within value. + /// A half-precision floating point number formed by two bytes beginning at startIndex. + /// + /// startIndex is greater than or equal to the length of value minus 1, and is + /// less than or equal to the length of value minus 1. + /// + /// value is null. + /// startIndex is less than zero or greater than the length of value minus 1. + public static Half ToHalf(byte[] value, int startIndex) + { + return ToHalf((ushort)BitConverter.ToInt16(value, startIndex)); + } + /// + /// Returns a half-precision floating point number converted from its binary representation. + /// + /// Binary representation of System.Half value + /// A half-precision floating point number formed by its binary representation. + public static Half ToHalf(ushort bits) + { + return new Half { Value = bits }; + } + + /// + /// Returns a value indicating the sign of a half-precision floating-point number. + /// + /// A signed number. + /// + /// A number indicating the sign of value. Number Description -1 value is less + /// than zero. 0 value is equal to zero. 1 value is greater than zero. + /// + /// value is equal to System.Half.NaN. + public static int Sign(Half value) + { + if (value < 0) + { + return -1; + } + else if (value > 0) + { + return 1; + } + else + { + if (value != 0) + { + throw new ArithmeticException("Function does not accept floating point Not-a-Number values."); + } + } + + return 0; + } + /// + /// Returns the absolute value of a half-precision floating-point number. + /// + /// A number in the range System.Half.MinValue ≤ value ≤ System.Half.MaxValue. + /// A half-precision floating-point number, x, such that 0 ≤ x ≤System.Half.MaxValue. + public static Half Abs(Half value) + { + return HalfHelper.Abs(value); + } + /// + /// Returns the larger of two half-precision floating-point numbers. + /// + /// The first of two half-precision floating-point numbers to compare. + /// The second of two half-precision floating-point numbers to compare. + /// + /// Parameter value1 or value2, whichever is larger. If value1, or value2, or both val1 + /// and value2 are equal to System.Half.NaN, System.Half.NaN is returned. + /// + public static Half Max(Half value1, Half value2) + { + return value1 < value2 ? value2 : value1; + } + /// + /// Returns the smaller of two half-precision floating-point numbers. + /// + /// The first of two half-precision floating-point numbers to compare. + /// The second of two half-precision floating-point numbers to compare. + /// + /// Parameter value1 or value2, whichever is smaller. If value1, or value2, or both val1 + /// and value2 are equal to System.Half.NaN, System.Half.NaN is returned. + /// + public static Half Min(Half value1, Half value2) + { + return value1 < value2 ? value1 : value2; + } + #endregion + + /// + /// Returns a value indicating whether the specified number evaluates to not a number (System.Half.NaN). + /// + /// A half-precision floating-point number. + /// true if value evaluates to not a number (System.Half.NaN); otherwise, false. + public static bool IsNaN(Half half) + { + return HalfHelper.IsNaN(half); + } + /// + /// Returns a value indicating whether the specified number evaluates to negative or positive infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.PositiveInfinity or System.Half.NegativeInfinity; otherwise, false. + public static bool IsInfinity(Half half) + { + return HalfHelper.IsInfinity(half); + } + /// + /// Returns a value indicating whether the specified number evaluates to negative infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.NegativeInfinity; otherwise, false. + public static bool IsNegativeInfinity(Half half) + { + return HalfHelper.IsNegativeInfinity(half); + } + /// + /// Returns a value indicating whether the specified number evaluates to positive infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.PositiveInfinity; otherwise, false. + public static bool IsPositiveInfinity(Half half) + { + return HalfHelper.IsPositiveInfinity(half); + } + + #region String operations (Parse and ToString) + /// + /// Converts the string representation of a number to its System.Half equivalent. + /// + /// The string representation of the number to convert. + /// The System.Half number equivalent to the number contained in value. + /// value is null. + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value) + { + return (Half)float.Parse(value, CultureInfo.InvariantCulture); + } + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified culture-specific format information. + /// + /// The string representation of the number to convert. + /// An System.IFormatProvider that supplies culture-specific parsing information about value. + /// The System.Half number equivalent to the number contained in s as specified by provider. + /// value is null. + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, IFormatProvider provider) + { + return (Half)float.Parse(value, provider); + } + /// + /// Converts the string representation of a number in a specified style to its System.Half equivalent. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the style elements that can be present in value. A typical value to specify is + /// System.Globalization.NumberStyles.Number. + /// + /// The System.Half number equivalent to the number contained in s as specified by style. + /// value is null. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is the + /// System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, NumberStyles style) + { + return (Half)float.Parse(value, style, CultureInfo.InvariantCulture); + } + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified style and culture-specific format. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the style elements that can be present in value. A typical value to specify is + /// System.Globalization.NumberStyles.Number. + /// + /// An System.IFormatProvider object that supplies culture-specific information about the format of value. + /// The System.Half number equivalent to the number contained in s as specified by style and provider. + /// value is null. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is the + /// System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, NumberStyles style, IFormatProvider provider) + { + return (Half)float.Parse(value, style, provider); + } + /// + /// Converts the string representation of a number to its System.Half equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// + /// The string representation of the number to convert. + /// + /// When this method returns, contains the System.Half number that is equivalent + /// to the numeric value contained in value, if the conversion succeeded, or is zero + /// if the conversion failed. The conversion fails if the s parameter is null, + /// is not a number in a valid format, or represents a number less than System.Half.MinValue + /// or greater than System.Half.MaxValue. This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string value, out Half result) + { + float f; + if (float.TryParse(value, out f)) + { + result = (Half)f; + return true; + } + + result = new Half(); + return false; + } + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified style and culture-specific format. A return value indicates + /// whether the conversion succeeded or failed. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the permitted format of value. A typical value to specify is System.Globalization.NumberStyles.Number. + /// + /// An System.IFormatProvider object that supplies culture-specific parsing information about value. + /// + /// When this method returns, contains the System.Half number that is equivalent + /// to the numeric value contained in value, if the conversion succeeded, or is zero + /// if the conversion failed. The conversion fails if the s parameter is null, + /// is not in a format compliant with style, or represents a number less than + /// System.Half.MinValue or greater than System.Half.MaxValue. This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style + /// is the System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + public static bool TryParse(string value, NumberStyles style, IFormatProvider provider, out Half result) + { + var parseResult = false; + float f; + if (float.TryParse(value, style, provider, out f)) + { + result = (Half)f; + parseResult = true; + } + else + { + result = new Half(); + } + + return parseResult; + } + /// + /// Converts the numeric value of this instance to its equivalent string representation. + /// + /// A string that represents the value of this instance. + public override string ToString() + { + return ((float)this).ToString(CultureInfo.InvariantCulture); + } + /// + /// Converts the numeric value of this instance to its equivalent string representation + /// using the specified culture-specific format information. + /// + /// An System.IFormatProvider that supplies culture-specific formatting information. + /// The string representation of the value of this instance as specified by provider. + public string ToString(IFormatProvider formatProvider) + { + return ((float)this).ToString(formatProvider); + } + /// + /// Converts the numeric value of this instance to its equivalent string representation, using the specified format. + /// + /// A numeric format string. + /// The string representation of the value of this instance as specified by format. + public string ToString(string format) + { + return ((float)this).ToString(format, CultureInfo.InvariantCulture); + } + /// + /// Converts the numeric value of this instance to its equivalent string representation + /// using the specified format and culture-specific format information. + /// + /// A numeric format string. + /// An System.IFormatProvider that supplies culture-specific formatting information. + /// The string representation of the value of this instance as specified by format and provider. + /// format is invalid. + public string ToString(string format, IFormatProvider formatProvider) + { + return ((float)this).ToString(format, formatProvider); + } + #endregion + + #region IConvertible Members + float IConvertible.ToSingle(IFormatProvider provider) + { + return this; + } + TypeCode IConvertible.GetTypeCode() + { + return GetTypeCode(); + } + bool IConvertible.ToBoolean(IFormatProvider provider) + { + return Convert.ToBoolean(this); + } + byte IConvertible.ToByte(IFormatProvider provider) + { + return Convert.ToByte(this); + } + char IConvertible.ToChar(IFormatProvider provider) + { + throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "Invalid cast from '{0}' to '{1}'.", "Half", "Char")); + } + DateTime IConvertible.ToDateTime(IFormatProvider provider) + { + throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "Invalid cast from '{0}' to '{1}'.", "Half", "DateTime")); + } + decimal IConvertible.ToDecimal(IFormatProvider provider) + { + return Convert.ToDecimal(this); + } + double IConvertible.ToDouble(IFormatProvider provider) + { + return Convert.ToDouble(this); + } + short IConvertible.ToInt16(IFormatProvider provider) + { + return Convert.ToInt16(this); + } + int IConvertible.ToInt32(IFormatProvider provider) + { + return Convert.ToInt32(this); + } + long IConvertible.ToInt64(IFormatProvider provider) + { + return Convert.ToInt64(this); + } + sbyte IConvertible.ToSByte(IFormatProvider provider) + { + return Convert.ToSByte(this); + } + string IConvertible.ToString(IFormatProvider provider) + { + return Convert.ToString(this, CultureInfo.InvariantCulture); + } + object IConvertible.ToType(Type conversionType, IFormatProvider provider) + { + return ((float)this as IConvertible).ToType(conversionType, provider); + } + ushort IConvertible.ToUInt16(IFormatProvider provider) + { + return Convert.ToUInt16(this); + } + uint IConvertible.ToUInt32(IFormatProvider provider) + { + return Convert.ToUInt32(this); + } + ulong IConvertible.ToUInt64(IFormatProvider provider) + { + return Convert.ToUInt64(this); + } + #endregion + } +} + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// Helper class for Half conversions and some low level operations. + /// This class is internally used in the Half class. + /// + /// + /// References: + /// - Code retrieved from http://sourceforge.net/p/csharp-half/code/HEAD/tree/ on 2015-12-04 + /// - Fast Half Float Conversions, Jeroen van der Zijp, link: http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf + /// + internal static class HalfHelper + { + private static readonly uint[] MantissaTable = GenerateMantissaTable(); + private static readonly uint[] ExponentTable = GenerateExponentTable(); + private static readonly ushort[] OffsetTable = GenerateOffsetTable(); + private static readonly ushort[] BaseTable = GenerateBaseTable(); + private static readonly sbyte[] ShiftTable = GenerateShiftTable(); + + // Transforms the subnormal representation to a normalized one. + private static uint ConvertMantissa(int i) + { + var m = (uint)(i << 13); // Zero pad mantissa bits + uint e = 0; // Zero exponent + + // While not normalized + while ((m & 0x00800000) == 0) + { + e -= 0x00800000; // Decrement exponent (1<<23) + m <<= 1; // Shift mantissa + } + m &= unchecked((uint)~0x00800000); // Clear leading 1 bit + e += 0x38800000; // Adjust bias ((127-14)<<23) + return m | e; // Return combined number + } + + private static uint[] GenerateMantissaTable() + { + var mantissaTable = new uint[2048]; + mantissaTable[0] = 0; + for (var i = 1; i < 1024; i++) + { + mantissaTable[i] = ConvertMantissa(i); + } + for (var i = 1024; i < 2048; i++) + { + mantissaTable[i] = (uint)(0x38000000 + (i - 1024 << 13)); + } + + return mantissaTable; + } + private static uint[] GenerateExponentTable() + { + var exponentTable = new uint[64]; + exponentTable[0] = 0; + for (var i = 1; i < 31; i++) + { + exponentTable[i] = (uint)(i << 23); + } + exponentTable[31] = 0x47800000; + exponentTable[32] = 0x80000000; + for (var i = 33; i < 63; i++) + { + exponentTable[i] = (uint)(0x80000000 + (i - 32 << 23)); + } + exponentTable[63] = 0xc7800000; + + return exponentTable; + } + private static ushort[] GenerateOffsetTable() + { + var offsetTable = new ushort[64]; + offsetTable[0] = 0; + for (var i = 1; i < 32; i++) + { + offsetTable[i] = 1024; + } + offsetTable[32] = 0; + for (var i = 33; i < 64; i++) + { + offsetTable[i] = 1024; + } + + return offsetTable; + } + private static ushort[] GenerateBaseTable() + { + var baseTable = new ushort[512]; + for (var i = 0; i < 256; ++i) + { + var e = (sbyte)(127 - i); + if (e > 24) + { // Very small numbers map to zero + baseTable[i | 0x000] = 0x0000; + baseTable[i | 0x100] = 0x8000; + } + else if (e > 14) + { // Small numbers map to denorms + baseTable[i | 0x000] = (ushort)(0x0400 >> 18 + e); + baseTable[i | 0x100] = (ushort)(0x0400 >> 18 + e | 0x8000); + } + else if (e >= -15) + { // Normal numbers just lose precision + baseTable[i | 0x000] = (ushort)(15 - e << 10); + baseTable[i | 0x100] = (ushort)(15 - e << 10 | 0x8000); + } + else if (e > -128) + { // Large numbers map to Infinity + baseTable[i | 0x000] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + } + else + { // Infinity and NaN's stay Infinity and NaN's + baseTable[i | 0x000] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + } + } + + return baseTable; + } + private static sbyte[] GenerateShiftTable() + { + var shiftTable = new sbyte[512]; + for (var i = 0; i < 256; ++i) + { + var e = (sbyte)(127 - i); + if (e > 24) + { // Very small numbers map to zero + shiftTable[i | 0x000] = 24; + shiftTable[i | 0x100] = 24; + } + else if (e > 14) + { // Small numbers map to denorms + shiftTable[i | 0x000] = (sbyte)(e - 1); + shiftTable[i | 0x100] = (sbyte)(e - 1); + } + else if (e >= -15) + { // Normal numbers just lose precision + shiftTable[i | 0x000] = 13; + shiftTable[i | 0x100] = 13; + } + else if (e > -128) + { // Large numbers map to Infinity + shiftTable[i | 0x000] = 24; + shiftTable[i | 0x100] = 24; + } + else + { // Infinity and NaN's stay Infinity and NaN's + shiftTable[i | 0x000] = 13; + shiftTable[i | 0x100] = 13; + } + } + + return shiftTable; + } + + public static unsafe float HalfToSingle(Half half) + { + var result = MantissaTable[OffsetTable[half.Value >> 10] + (half.Value & 0x3ff)] + ExponentTable[half.Value >> 10]; + return *(float*)&result; + } + public static unsafe Half SingleToHalf(float single) + { + var value = *(uint*)&single; + + var result = (ushort)(BaseTable[value >> 23 & 0x1ff] + ((value & 0x007fffff) >> ShiftTable[value >> 23])); + return Half.ToHalf(result); + } + + public static Half Negate(Half half) + { + return Half.ToHalf((ushort)(half.Value ^ 0x8000)); + } + public static Half Abs(Half half) + { + return Half.ToHalf((ushort)(half.Value & 0x7fff)); + } + + public static bool IsNaN(Half half) + { + return (half.Value & 0x7fff) > 0x7c00; + } + public static bool IsInfinity(Half half) + { + return (half.Value & 0x7fff) == 0x7c00; + } + public static bool IsPositiveInfinity(Half half) + { + return half.Value == 0x7c00; + } + public static bool IsNegativeInfinity(Half half) + { + return half.Value == 0xfc00; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/HdrImage.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/HdrImage.cs new file mode 100644 index 0000000..e1084a3 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/HdrImage.cs @@ -0,0 +1,342 @@ +using CommunityToolkit.HighPerformance; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// Reads and writes .hdr RGBE/Radiance HDR files. File format by Gregory Ward. + /// This class is experimental, incomplete and probably going to be removed in a future version. + /// Use only if you don't have anything better. + /// + public class HdrImage + { + public enum ColorSpace + { + Rgbe, + Xyze + } + + public float exposure = -1; + public float gamma = -1; + public int width; + public int height; + + public ColorRgbFloat[] pixels; + + /// + /// Gets a span2D over the array. + /// + public Span2D PixelSpan => new(pixels, height, width); + /// + /// Gets a span2D over the array. + /// + public Memory2D PixelMemory => new(pixels, height, width); + + public HdrImage() { } + + public HdrImage(int width, int height, float exposure = 0, float gamma = 0) + { + this.width = width; + this.height = height; + this.exposure = exposure; + this.gamma = gamma; + pixels = new ColorRgbFloat[width * height]; + } + + public HdrImage(Span2D pixels, float exposure = 0, float gamma = 0) + { + width = pixels.Width; + height = pixels.Height; + this.exposure = exposure; + this.gamma = gamma; + this.pixels = new ColorRgbFloat[width * height]; + pixels.CopyTo(this.pixels); + } + + // StreamReader class does not work. Have to use custom string reading. + private static string ReadFromStream(Stream stream) + { + var i = 0; + var buffer = new char[512]; + char c; + do + { + var b = stream.ReadByte(); + if (b == -1) + { + return null; + } + c = (char)b; + buffer[i++] = c; + } while (c != (char)10); + return new string(buffer.AsSpan()[..i]).Trim(); + } + + private static void WriteLineToStream(BinaryWriter br, string s) + { + foreach (var c in s) + { + var b = (byte)c; + br.Write(b); + } + br.Write((byte)10); + } + + /// + /// Read a Radiance HDR image by filename. + /// Just calls internally. + /// + /// The filename or path of the image + /// A new HdrImage with the data + public static HdrImage Read(string filename) + { + using var fs = File.OpenRead(filename); + return Read(fs); + } + + /// + /// Read a Radiance HDR image from a stream + /// + /// The stream to read from + /// A new HdrImage with the data + public static HdrImage Read(Stream stream) + { + var image = new HdrImage(); + + var line = ReadFromStream(stream); + + if (!(line == "#?RGBE" || line == "#?RADIANCE" || line == "#?AUTOPANO")) + { + throw new FileLoadException("Correct file type specifier was not found."); + } + + var colorSpace = ColorSpace.Rgbe; + + do + { + line = ReadFromStream(stream); + + if (line == null) + { + throw new FileLoadException("Reached end of stream."); + } + + line = line.Trim(); + + if (line == "") + { + break; + } + + if (line.StartsWith('#')) // Found comment + { + continue; + } + + if (line == "FORMAT=32-bit_rle_rgbe") + { + colorSpace = ColorSpace.Rgbe; + } + + else if (line == "FORMAT=32-bit_rle_xyze") + { + colorSpace = ColorSpace.Xyze; + } + + else if (line.StartsWith("EXPOSURE=")) + { + image.exposure = float.Parse(line.Replace("EXPOSURE=", "").Trim(), CultureInfo.InvariantCulture); + } + + else if (line.StartsWith("GAMMA=")) + { + image.gamma = float.Parse(line.Replace("GAMMA=", "").Trim(), CultureInfo.InvariantCulture); + } + + } while (true); + + if (image.exposure < 0.000001) + { + image.exposure = 1.0f; + } + + if (image.gamma < 0.000001) + { + image.gamma = 1.0f; + } + + var imgSize = ReadFromStream(stream).Split(' '); + + //var yStr = imgSize[0]; + image.height = int.Parse(imgSize[1]); + //var xStr = imgSize[2]; + image.width = int.Parse(imgSize[3]); + + ReadPixels(image, stream); + + if (colorSpace == ColorSpace.Xyze) + { + // Transform colorspace + var xyzColors = MemoryMarshal.Cast(image.pixels.AsSpan()); + for (var i = 0; i < xyzColors.Length; i++) + { + image.pixels[i] = xyzColors[i].ToColorRgbFloat(); + } + } + + return image; + } + + + private static void RleReadChannel(BinaryReader br, Span dest, int width) + { + var i = 0; + var data = new byte[2]; + while (i < width) + { + if (br.Read(data) == 0) + { + throw new FileLoadException("Not enough data in RLE"); + } + if (data[0] > 128) + { + // same byte is repeated many times + var len = data[0] - 128; + for (; len > 0; len--) + { + dest[i++] = data[1]; + } + } + else + { + // different byte sequence + dest[i++] = data[1]; + + var len = data[0] - 1; + if (len > 0) + { + if (br.Read(dest.Slice(i, len)) == 0) + { + throw new FileLoadException("Not enough data in RLE"); + } + i += len; + } + } + } + + if (i != width) + { + throw new FileLoadException("Scanline size was different from width"); + } + } + + + private static void ReadPixels(HdrImage destImage, Stream stream) + { + var height = destImage.height; + var width = destImage.width; + destImage.pixels = new ColorRgbFloat[destImage.height * destImage.width]; + Span bytes = new byte[destImage.width * 4]; + + using var br = new BinaryReader(stream, Encoding.ASCII, true); + + var header = new byte[4]; + + for (var y = 0; y < height; y++) + { + br.Read(header); + + var isRle = header[0] == 2 && header[1] == 2 && + (header[2] << 8) + header[3] == width; // whether the scanline is rle or not + + if (isRle) + { + // for each channel + for (var i = 0; i < 4; i++) + { + RleReadChannel(br, bytes.Slice(width * i, width), width); + } + + for (var x = 0; x < width; x++) + { + var color = new ColorRgbe( + bytes[x + width * 0], + bytes[x + width * 1], + bytes[x + width * 2], + bytes[x + width * 3] + ); + + destImage.pixels[y * width + x] = color.ToColorRgbFloat(destImage.exposure); + } + } + else + { + br.Read(bytes[4..]); + header.CopyTo(bytes); + + for (var x = 0; x < width; x++) + { + var color = new ColorRgbe( + bytes[4 * x + 0], + bytes[4 * x + 1], + bytes[4 * x + 2], + bytes[4 * x + 3] + ); + + destImage.pixels[y * width + x] = color.ToColorRgbFloat(destImage.exposure); + } + } + } + } + + /// + /// Write this file to a stream. + /// + /// The stream to write it to. + public void Write(Stream stream) + { + using var br = new BinaryWriter(stream, Encoding.ASCII, true); + + WriteLineToStream(br, "#?RADIANCE"); + WriteLineToStream(br, "# BCnEncoder.Net HdrImage"); + WriteLineToStream(br, "FORMAT=32-bit_rle_rgbe"); + if (exposure > 0) + { + WriteLineToStream(br, "EXPOSURE=" + exposure.ToString(CultureInfo.InvariantCulture)); + } + if (gamma > 0) + { + WriteLineToStream(br, "GAMMA=" + gamma.ToString(CultureInfo.InvariantCulture)); + } + + WriteLineToStream(br, ""); // Start data with empty row + WriteLineToStream(br, $"-Y {height} +X {width}"); + + WritePixels(br); + } + + private void WritePixels(BinaryWriter br) + { + var buffer = new byte[4]; + var span = PixelSpan; + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var pixel = span[y, x]; + var rgbe = new ColorRgbe(pixel); + + buffer[0] = rgbe.r; + buffer[1] = rgbe.g; + buffer[2] = rgbe.b; + buffer[3] = rgbe.e; + + br.Write(buffer); + } + } + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/DdsFile.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/DdsFile.cs new file mode 100644 index 0000000..287b1f0 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/DdsFile.cs @@ -0,0 +1,947 @@ +using System.Runtime.InteropServices; +using System.Text; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles +{ + public class DdsFile + { + public DdsHeader header; + public DdsHeaderDx10 dx10Header; + public List Faces { get; } = []; + + public DdsFile() { } + public DdsFile(DdsHeader header) + { + this.header = header; + } + + public DdsFile(DdsHeader header, DdsHeaderDx10 dx10Header) + { + this.header = header; + this.dx10Header = dx10Header; + } + + public static DdsFile Load(Stream s) + { + using var br = new BinaryReader(s, Encoding.UTF8, true); + var magic = br.ReadUInt32(); + if (magic != 0x20534444U) + { + throw new FormatException("The file does not contain a dds file."); + } + var header = br.ReadStruct(); + DdsHeaderDx10 dx10Header = default; + if (header.dwSize != 124) + { + throw new FormatException("The file header contains invalid dwSize."); + } + + var dx10Format = header.ddsPixelFormat.IsDxt10Format; + + DdsFile output; + + if (dx10Format) + { + dx10Header = br.ReadStruct(); + output = new DdsFile(header, dx10Header); + } + else + { + output = new DdsFile(header); + } + + var mipMapCount = Math.Max(1, header.dwMipMapCount); + var faceCount = (header.dwCaps2 & HeaderCaps2.Ddscaps2Cubemap) != 0 ? 6u : 1u; + var width = header.dwWidth; + var height = header.dwHeight; + + for (var face = 0; face < faceCount; face++) + { + var format = dx10Format ? dx10Header.dxgiFormat : header.ddsPixelFormat.DxgiFormat; + var sizeInBytes = GetSizeInBytes(format, width, height); + + output.Faces.Add(new DdsFace(width, height, sizeInBytes, (int)mipMapCount)); + + for (var mip = 0; mip < mipMapCount; mip++) + { + MipMapper.CalculateMipLevelSize( + (int)header.dwWidth, + (int)header.dwHeight, + mip, + out var mipWidth, + out var mipHeight); + + if (mip > 0) //Calculate new byteSize + { + sizeInBytes = GetSizeInBytes(format, (uint)mipWidth, (uint)mipHeight); + } + + var data = new byte[sizeInBytes]; + br.Read(data); + output.Faces[face].MipMaps[mip] = new DdsMipMap(data, (uint)mipWidth, (uint)mipHeight); + } + } + + return output; + } + + public void Write(Stream outputStream) + { + if (Faces.Count < 1 || Faces[0].MipMaps.Length < 1) + { + throw new InvalidOperationException("The DDS structure should have at least 1 mipmap level and 1 Face before writing to file."); + } + + header.dwFlags |= HeaderFlags.Required; + + header.dwMipMapCount = (uint)Faces[0].MipMaps.Length; + if (header.dwMipMapCount > 1) // MipMaps + { + header.dwCaps |= HeaderCaps.DdscapsMipmap | HeaderCaps.DdscapsComplex; + } + if (Faces.Count == 6) // CubeMap + { + header.dwCaps |= HeaderCaps.DdscapsComplex; + header.dwCaps2 |= HeaderCaps2.Ddscaps2Cubemap | + HeaderCaps2.Ddscaps2CubemapPositivex | + HeaderCaps2.Ddscaps2CubemapNegativex | + HeaderCaps2.Ddscaps2CubemapPositivey | + HeaderCaps2.Ddscaps2CubemapNegativey | + HeaderCaps2.Ddscaps2CubemapPositivez | + HeaderCaps2.Ddscaps2CubemapNegativez; + } + + header.dwWidth = Faces[0].Width; + header.dwHeight = Faces[0].Height; + + for (var i = 0; i < Faces.Count; i++) + { + if (Faces[i].Width != header.dwWidth || Faces[i].Height != header.dwHeight) + { + throw new InvalidOperationException("Faces with different sizes are not supported."); + } + } + + var faceCount = Faces.Count; + var mipCount = (int)header.dwMipMapCount; + + using var bw = new BinaryWriter(outputStream, Encoding.UTF8, true); + bw.Write(0x20534444U); // magic 'DDS ' + + bw.WriteStruct(header); + + if (header.ddsPixelFormat.IsDxt10Format) + { + bw.WriteStruct(dx10Header); + } + + for (var face = 0; face < faceCount; face++) + { + for (var mip = 0; mip < mipCount; mip++) + { + bw.Write(Faces[face].MipMaps[mip].Data); + } + } + } + + public static uint GetSizeInBytes(DxgiFormat format, uint width, uint height) + { + uint sizeInBytes; + if (format.IsCompressedFormat()) + { + sizeInBytes = (uint)ImageToBlocks.CalculateNumOfBlocks((int)width, (int)height); + sizeInBytes *= (uint)format.GetByteSize(); + } + else + { + sizeInBytes = width * height; + sizeInBytes = (uint)(sizeInBytes * format.GetByteSize()); + } + + return sizeInBytes; + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct DdsHeader + { + /// + /// Has to be 124 + /// + public uint dwSize; + public HeaderFlags dwFlags; + public uint dwHeight; + public uint dwWidth; + public uint dwPitchOrLinearSize; + public uint dwDepth; + public uint dwMipMapCount; + public fixed uint dwReserved1[11]; + public DdsPixelFormat ddsPixelFormat; + public HeaderCaps dwCaps; + public HeaderCaps2 dwCaps2; + public uint dwCaps3; + public uint dwCaps4; + public uint dwReserved2; + + public static (DdsHeader, DdsHeaderDx10) InitializeCompressed(int width, int height, DxgiFormat format, bool preferDxt10Header) + { + var header = new DdsHeader(); + var dxt10Header = new DdsHeaderDx10(); + + header.dwSize = 124; + header.dwFlags = HeaderFlags.Required; + header.dwWidth = (uint)width; + header.dwHeight = (uint)height; + header.dwDepth = 1; + header.dwMipMapCount = 1; + header.dwCaps = HeaderCaps.DdscapsTexture; + + if (preferDxt10Header) + { + // ATC formats cannot be written to DXT10 header due to lack of a DxgiFormat enum + switch (format) + { + case DxgiFormat.DxgiFormatAtcExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atc + }; + break; + + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atci + }; + break; + + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atca + }; + break; + + default: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dx10 + }; + dxt10Header.arraySize = 1; + dxt10Header.dxgiFormat = format; + dxt10Header.resourceDimension = D3D10ResourceDimension.D3D10ResourceDimensionTexture2D; + break; + } + } + else + { + switch (format) + { + case DxgiFormat.DxgiFormatBc1Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dxt1 + }; + break; + + case DxgiFormat.DxgiFormatBc2Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dxt3 + }; + break; + + case DxgiFormat.DxgiFormatBc3Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dxt5 + }; + break; + + case DxgiFormat.DxgiFormatBc4Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Bc4U + }; + break; + + case DxgiFormat.DxgiFormatBc5Unorm: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Ati2 + }; + break; + + case DxgiFormat.DxgiFormatAtcExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atc + }; + break; + + case DxgiFormat.DxgiFormatAtcExplicitAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atci + }; + break; + + case DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Atca + }; + break; + + default: + header.ddsPixelFormat = new DdsPixelFormat + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfFourcc, + dwFourCc = DdsPixelFormat.Dx10 + }; + dxt10Header.arraySize = 1; + dxt10Header.dxgiFormat = format; + dxt10Header.resourceDimension = D3D10ResourceDimension.D3D10ResourceDimensionTexture2D; + break; + } + } + + return (header, dxt10Header); + } + + public static DdsHeader InitializeUncompressed(int width, int height, DxgiFormat format) + { + var header = new DdsHeader + { + dwSize = 124, + dwFlags = HeaderFlags.Required | HeaderFlags.DdsdPitch, + dwWidth = (uint)width, + dwHeight = (uint)height, + dwDepth = 1, + dwMipMapCount = 1, + dwCaps = HeaderCaps.DdscapsTexture + }; + + if (format == DxgiFormat.DxgiFormatR8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfLuminance, + dwRgbBitCount = 8, + dwRBitMask = 0xFF + }; + header.dwPitchOrLinearSize = (uint)((width * 8 + 7) / 8); + } + else if (format == DxgiFormat.DxgiFormatR8G8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfLuminance | PixelFormatFlags.DdpfAlphaPixels, + dwRgbBitCount = 16, + dwRBitMask = 0xFF, + dwGBitMask = 0xFF00 + }; + header.dwPitchOrLinearSize = (uint)((width * 16 + 7) / 8); + } + else if (format == DxgiFormat.DxgiFormatR8G8B8A8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfRgb | PixelFormatFlags.DdpfAlphaPixels, + dwRgbBitCount = 32, + dwRBitMask = 0xFF, + dwGBitMask = 0xFF00, + dwBBitMask = 0xFF0000, + dwABitMask = 0xFF000000, + }; + header.dwPitchOrLinearSize = (uint)((width * 32 + 7) / 8); + } + else if (format == DxgiFormat.DxgiFormatB8G8R8A8Unorm) + { + header.ddsPixelFormat = new DdsPixelFormat() + { + dwSize = 32, + dwFlags = PixelFormatFlags.DdpfRgb | PixelFormatFlags.DdpfAlphaPixels, + dwRgbBitCount = 32, + dwRBitMask = 0xFF0000, + dwGBitMask = 0xFF00, + dwBBitMask = 0xFF, + dwABitMask = 0xFF000000, + }; + header.dwPitchOrLinearSize = (uint)((width * 32 + 7) / 8); + } + else + { + throw new NotImplementedException("This Format is not implemented in this method"); + } + + return header; + } + } + + public struct DdsPixelFormat + { + public static readonly uint Dx10 = MakeFourCc('D', 'X', '1', '0'); + + public static readonly uint Dxt1 = MakeFourCc('D', 'X', 'T', '1'); + public static readonly uint Dxt2 = MakeFourCc('D', 'X', 'T', '2'); + public static readonly uint Dxt3 = MakeFourCc('D', 'X', 'T', '3'); + public static readonly uint Dxt4 = MakeFourCc('D', 'X', 'T', '4'); + public static readonly uint Dxt5 = MakeFourCc('D', 'X', 'T', '5'); + public static readonly uint Ati1 = MakeFourCc('A', 'T', 'I', '1'); + public static readonly uint Ati2 = MakeFourCc('A', 'T', 'I', '2'); + public static readonly uint Atc = MakeFourCc('A', 'T', 'C', ' '); + public static readonly uint Atci = MakeFourCc('A', 'T', 'C', 'I'); + public static readonly uint Atca = MakeFourCc('A', 'T', 'C', 'A'); + + public static readonly uint Bc4S = MakeFourCc('B', 'C', '4', 'S'); + public static readonly uint Bc4U = MakeFourCc('B', 'C', '4', 'U'); + public static readonly uint Bc5S = MakeFourCc('B', 'C', '5', 'S'); + public static readonly uint Bc5U = MakeFourCc('B', 'C', '5', 'U'); + + private static uint MakeFourCc(char c0, char c1, char c2, char c3) + { + uint result = c0; + result |= (uint)c1 << 8; + result |= (uint)c2 << 16; + result |= (uint)c3 << 24; + return result; + } + + public uint dwSize; + public PixelFormatFlags dwFlags; + public uint dwFourCc; + public uint dwRgbBitCount; + public uint dwRBitMask; + public uint dwGBitMask; + public uint dwBBitMask; + public uint dwABitMask; + + public readonly DxgiFormat DxgiFormat + { + get + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfFourcc)) + { + if (dwFourCc == Dxt1) return DxgiFormat.DxgiFormatBc1Unorm; + if (dwFourCc == Dxt2 || dwFourCc == Dxt3) return DxgiFormat.DxgiFormatBc2Unorm; + if (dwFourCc == Dxt4 || dwFourCc == Dxt5) return DxgiFormat.DxgiFormatBc3Unorm; + if (dwFourCc == Ati1 || dwFourCc == Bc4S || dwFourCc == Bc4U) return DxgiFormat.DxgiFormatBc4Unorm; + if (dwFourCc == Ati2 || dwFourCc == Bc5S || dwFourCc == Bc5U) return DxgiFormat.DxgiFormatBc5Unorm; + if (dwFourCc == Atc) return DxgiFormat.DxgiFormatAtcExt; + if (dwFourCc == Atci) return DxgiFormat.DxgiFormatAtcExplicitAlphaExt; + if (dwFourCc == Atca) return DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt; + } + else + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfRgb)) // RGB/A + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels)) //RGBA + { + if (dwRgbBitCount == 32) + { + if (dwRBitMask == 0xff && dwGBitMask == 0xff00 && dwBBitMask == 0xff0000 && + dwABitMask == 0xff000000) + { + return DxgiFormat.DxgiFormatR8G8B8A8Unorm; + } + + if (dwRBitMask == 0xff0000 && dwGBitMask == 0xff00 && dwBBitMask == 0xff && + dwABitMask == 0xff000000) + { + return DxgiFormat.DxgiFormatB8G8R8A8Unorm; + } + } + } + else //RGB + { + if (dwRgbBitCount == 32) + { + if (dwRBitMask == 0xff0000 && dwGBitMask == 0xff00 && dwBBitMask == 0xff) + { + return DxgiFormat.DxgiFormatB8G8R8X8Unorm; + } + } + } + } + else if (dwFlags.HasFlag(PixelFormatFlags.DdpfLuminance)) // R/RG + { + if (dwFlags.HasFlag(PixelFormatFlags.DdpfAlphaPixels)) // RG + { + if (dwRgbBitCount == 16) + { + if (dwRBitMask == 0xff && dwGBitMask == 0xff00) + { + return DxgiFormat.DxgiFormatR8G8Unorm; + } + } + } + else // Luminance only + { + if (dwRgbBitCount == 8) + { + if (dwRBitMask == 0xff) + { + return DxgiFormat.DxgiFormatR8Unorm; + } + } + } + } + } + return DxgiFormat.DxgiFormatUnknown; + } + } + + public readonly bool IsDxt10Format => (dwFlags & PixelFormatFlags.DdpfFourcc) == PixelFormatFlags.DdpfFourcc + && dwFourCc == Dx10; + } + + public struct DdsHeaderDx10 + { + public DxgiFormat dxgiFormat; + public D3D10ResourceDimension resourceDimension; + public uint miscFlag; + public uint arraySize; + public uint miscFlags2; + } + + public class DdsFace(uint width, uint height, uint sizeInBytes, int numMipMaps) + { + public uint Width { get; set; } = width; + public uint Height { get; set; } = height; + public uint SizeInBytes { get; } = sizeInBytes; + public DdsMipMap[] MipMaps { get; } = new DdsMipMap[numMipMaps]; + } + + public class DdsMipMap(byte[] data, uint width, uint height) + { + public uint Width { get; set; } = width; + public uint Height { get; set; } = height; + public uint SizeInBytes { get; } = (uint)data.Length; + public byte[] Data { get; } = data; + } + + /// + /// Flags to indicate which members contain valid data. + /// + [Flags] + public enum HeaderFlags : uint + { + /// + /// Required in every .dds file. + /// + DdsdCaps = 0x1, + /// + /// Required in every .dds file. + /// + DdsdHeight = 0x2, + /// + /// Required in every .dds file. + /// + DdsdWidth = 0x4, + /// + /// Required when pitch is provided for an uncompressed texture. + /// + DdsdPitch = 0x8, + /// + /// Required in every .dds file. + /// + DdsdPixelformat = 0x1000, + /// + /// Required in a mipmapped texture. + /// + DdsdMipmapcount = 0x20000, + /// + /// Required when pitch is provided for a compressed texture. + /// + DdsdLinearsize = 0x80000, + /// + /// Required in a depth texture. + /// + DdsdDepth = 0x800000, + + Required = DdsdCaps | DdsdHeight | DdsdWidth | DdsdPixelformat + } + + /// + /// Specifies the complexity of the surfaces stored. + /// + [Flags] + public enum HeaderCaps : uint + { + /// + /// Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or mipmapped volume texture). + /// + DdscapsComplex = 0x8, + /// + /// Optional; should be used for a mipmap. + /// + DdscapsMipmap = 0x400000, + /// + /// Required + /// + DdscapsTexture = 0x1000 + } + + /// + /// Additional detail about the surfaces stored. + /// + [Flags] + public enum HeaderCaps2 : uint + { + /// + /// Required for a cube map. + /// + Ddscaps2Cubemap = 0x200, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapPositivex = 0x400, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapNegativex = 0x800, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapPositivey = 0x1000, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapNegativey = 0x2000, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapPositivez = 0x4000, + /// + /// Required when these surfaces are stored in a cube map. + /// + Ddscaps2CubemapNegativez = 0x8000, + /// + /// Required for a volume texture. + /// + Ddscaps2Volume = 0x200000 + } + + [Flags] + public enum PixelFormatFlags : uint + { + /// + /// Texture contains alpha data; dwRGBAlphaBitMask contains valid data. + /// + DdpfAlphaPixels = 0x1, + /// + /// Used in some older DDS files for alpha channel only uncompressed data (dwRGBBitCount contains the alpha channel bitcount; dwABitMask contains valid data) + /// + DdpfAlpha = 0x2, + /// + /// Texture contains compressed RGB data; dwFourCC contains valid data. + /// + DdpfFourcc = 0x4, + /// + /// Texture contains uncompressed RGB data; dwRGBBitCount and the RGB masks (dwRBitMask, dwGBitMask, dwBBitMask) contain valid data. + /// + DdpfRgb = 0x40, + /// + /// Used in some older DDS files for YUV uncompressed data (dwRGBBitCount contains the YUV bit count; dwRBitMask contains the Y mask, dwGBitMask contains the U mask, dwBBitMask contains the V mask) + /// + DdpfYuv = 0x200, + /// + /// Used in some older DDS files for single channel color uncompressed data (dwRGBBitCount contains the luminance channel bit count; dwRBitMask contains the channel mask). Can be combined with DDPF_ALPHAPIXELS for a two channel DDS file. + /// + DdpfLuminance = 0x20000 + } + + public enum D3D10ResourceDimension : uint + { + D3D10ResourceDimensionUnknown, + D3D10ResourceDimensionBuffer, + D3D10ResourceDimensionTexture1D, + D3D10ResourceDimensionTexture2D, + D3D10ResourceDimensionTexture3D + }; + + public enum DxgiFormat : uint + { + DxgiFormatUnknown, + DxgiFormatR32G32B32A32Typeless, + DxgiFormatR32G32B32A32Float, + DxgiFormatR32G32B32A32Uint, + DxgiFormatR32G32B32A32Sint, + DxgiFormatR32G32B32Typeless, + DxgiFormatR32G32B32Float, + DxgiFormatR32G32B32Uint, + DxgiFormatR32G32B32Sint, + DxgiFormatR16G16B16A16Typeless, + DxgiFormatR16G16B16A16Float, + DxgiFormatR16G16B16A16Unorm, + DxgiFormatR16G16B16A16Uint, + DxgiFormatR16G16B16A16Snorm, + DxgiFormatR16G16B16A16Sint, + DxgiFormatR32G32Typeless, + DxgiFormatR32G32Float, + DxgiFormatR32G32Uint, + DxgiFormatR32G32Sint, + DxgiFormatR32G8X24Typeless, + DxgiFormatD32FloatS8X24Uint, + DxgiFormatR32FloatX8X24Typeless, + DxgiFormatX32TypelessG8X24Uint, + DxgiFormatR10G10B10A2Typeless, + DxgiFormatR10G10B10A2Unorm, + DxgiFormatR10G10B10A2Uint, + DxgiFormatR11G11B10Float, + DxgiFormatR8G8B8A8Typeless, + DxgiFormatR8G8B8A8Unorm, + DxgiFormatR8G8B8A8UnormSrgb, + DxgiFormatR8G8B8A8Uint, + DxgiFormatR8G8B8A8Snorm, + DxgiFormatR8G8B8A8Sint, + DxgiFormatR16G16Typeless, + DxgiFormatR16G16Float, + DxgiFormatR16G16Unorm, + DxgiFormatR16G16Uint, + DxgiFormatR16G16Snorm, + DxgiFormatR16G16Sint, + DxgiFormatR32Typeless, + DxgiFormatD32Float, + DxgiFormatR32Float, + DxgiFormatR32Uint, + DxgiFormatR32Sint, + DxgiFormatR24G8Typeless, + DxgiFormatD24UnormS8Uint, + DxgiFormatR24UnormX8Typeless, + DxgiFormatX24TypelessG8Uint, + DxgiFormatR8G8Typeless, + DxgiFormatR8G8Unorm, + DxgiFormatR8G8Uint, + DxgiFormatR8G8Snorm, + DxgiFormatR8G8Sint, + DxgiFormatR16Typeless, + DxgiFormatR16Float, + DxgiFormatD16Unorm, + DxgiFormatR16Unorm, + DxgiFormatR16Uint, + DxgiFormatR16Snorm, + DxgiFormatR16Sint, + DxgiFormatR8Typeless, + DxgiFormatR8Unorm, + DxgiFormatR8Uint, + DxgiFormatR8Snorm, + DxgiFormatR8Sint, + DxgiFormatA8Unorm, + DxgiFormatR1Unorm, + DxgiFormatR9G9B9E5Sharedexp, + DxgiFormatR8G8B8G8Unorm, + DxgiFormatG8R8G8B8Unorm, + DxgiFormatBc1Typeless, + DxgiFormatBc1Unorm, + DxgiFormatBc1UnormSrgb, + DxgiFormatBc2Typeless, + DxgiFormatBc2Unorm, + DxgiFormatBc2UnormSrgb, + DxgiFormatBc3Typeless, + DxgiFormatBc3Unorm, + DxgiFormatBc3UnormSrgb, + DxgiFormatBc4Typeless, + DxgiFormatBc4Unorm, + DxgiFormatBc4Snorm, + DxgiFormatBc5Typeless, + DxgiFormatBc5Unorm, + DxgiFormatBc5Snorm, + DxgiFormatB5G6R5Unorm, + DxgiFormatB5G5R5A1Unorm, + DxgiFormatB8G8R8A8Unorm, + DxgiFormatB8G8R8X8Unorm, + DxgiFormatR10G10B10XrBiasA2Unorm, + DxgiFormatB8G8R8A8Typeless, + DxgiFormatB8G8R8A8UnormSrgb, + DxgiFormatB8G8R8X8Typeless, + DxgiFormatB8G8R8X8UnormSrgb, + DxgiFormatBc6HTypeless, + DxgiFormatBc6HUf16, + DxgiFormatBc6HSf16, + DxgiFormatBc7Typeless, + DxgiFormatBc7Unorm, + DxgiFormatBc7UnormSrgb, + DxgiFormatAyuv, + DxgiFormatY410, + DxgiFormatY416, + DxgiFormatNv12, + DxgiFormatP010, + DxgiFormatP016, + DxgiFormat420Opaque, + DxgiFormatYuy2, + DxgiFormatY210, + DxgiFormatY216, + DxgiFormatNv11, + DxgiFormatAi44, + DxgiFormatIa44, + DxgiFormatP8, + DxgiFormatA8P8, + DxgiFormatB4G4R4A4Unorm, + DxgiFormatP208, + DxgiFormatV208, + DxgiFormatV408, + DxgiFormatForceUint, + + // Added here due to lack of an official value + DxgiFormatAtcExt = 300, + DxgiFormatAtcExplicitAlphaExt, + DxgiFormatAtcInterpolatedAlphaExt + }; + + public static class DxgiFormatExtensions + { + public static int GetByteSize(this DxgiFormat format) + { + return format switch + { + DxgiFormat.DxgiFormatUnknown => 4, + DxgiFormat.DxgiFormatR32G32B32A32Typeless => 4 * 4, + DxgiFormat.DxgiFormatR32G32B32A32Float => 4 * 4, + DxgiFormat.DxgiFormatR32G32B32A32Uint => 4 * 4, + DxgiFormat.DxgiFormatR32G32B32A32Sint => 4 * 4, + DxgiFormat.DxgiFormatR32G32B32Typeless => 4 * 3, + DxgiFormat.DxgiFormatR32G32B32Float => 4 * 3, + DxgiFormat.DxgiFormatR32G32B32Uint => 4 * 3, + DxgiFormat.DxgiFormatR32G32B32Sint => 4 * 3, + DxgiFormat.DxgiFormatR16G16B16A16Typeless => 4 * 2, + DxgiFormat.DxgiFormatR16G16B16A16Float => 4 * 2, + DxgiFormat.DxgiFormatR16G16B16A16Unorm => 4 * 2, + DxgiFormat.DxgiFormatR16G16B16A16Uint => 4 * 2, + DxgiFormat.DxgiFormatR16G16B16A16Snorm => 4 * 2, + DxgiFormat.DxgiFormatR16G16B16A16Sint => 4 * 2, + DxgiFormat.DxgiFormatR32G32Typeless => 4 * 2, + DxgiFormat.DxgiFormatR32G32Float => 4 * 2, + DxgiFormat.DxgiFormatR32G32Uint => 4 * 2, + DxgiFormat.DxgiFormatR32G32Sint => 4 * 2, + DxgiFormat.DxgiFormatR32G8X24Typeless => 4 * 2, + DxgiFormat.DxgiFormatD32FloatS8X24Uint => 4, + DxgiFormat.DxgiFormatR32FloatX8X24Typeless => 4, + DxgiFormat.DxgiFormatX32TypelessG8X24Uint => 4, + DxgiFormat.DxgiFormatR10G10B10A2Typeless => 4, + DxgiFormat.DxgiFormatR10G10B10A2Unorm => 4, + DxgiFormat.DxgiFormatR10G10B10A2Uint => 4, + DxgiFormat.DxgiFormatR11G11B10Float => 4, + DxgiFormat.DxgiFormatR8G8B8A8Typeless => 4, + DxgiFormat.DxgiFormatR8G8B8A8Unorm => 4, + DxgiFormat.DxgiFormatR8G8B8A8UnormSrgb => 4, + DxgiFormat.DxgiFormatR8G8B8A8Uint => 4, + DxgiFormat.DxgiFormatR8G8B8A8Snorm => 4, + DxgiFormat.DxgiFormatR8G8B8A8Sint => 4, + DxgiFormat.DxgiFormatR16G16Typeless => 4, + DxgiFormat.DxgiFormatR16G16Float => 4, + DxgiFormat.DxgiFormatR16G16Unorm => 4, + DxgiFormat.DxgiFormatR16G16Uint => 4, + DxgiFormat.DxgiFormatR16G16Snorm => 4, + DxgiFormat.DxgiFormatR16G16Sint => 4, + DxgiFormat.DxgiFormatR32Typeless => 4, + DxgiFormat.DxgiFormatD32Float => 4, + DxgiFormat.DxgiFormatR32Float => 4, + DxgiFormat.DxgiFormatR32Uint => 4, + DxgiFormat.DxgiFormatR32Sint => 4, + DxgiFormat.DxgiFormatR24G8Typeless => 4, + DxgiFormat.DxgiFormatD24UnormS8Uint => 4, + DxgiFormat.DxgiFormatR24UnormX8Typeless => 4, + DxgiFormat.DxgiFormatX24TypelessG8Uint => 4, + DxgiFormat.DxgiFormatR8G8Typeless => 2, + DxgiFormat.DxgiFormatR8G8Unorm => 2, + DxgiFormat.DxgiFormatR8G8Uint => 2, + DxgiFormat.DxgiFormatR8G8Snorm => 2, + DxgiFormat.DxgiFormatR8G8Sint => 2, + DxgiFormat.DxgiFormatR16Typeless => 2, + DxgiFormat.DxgiFormatR16Float => 2, + DxgiFormat.DxgiFormatD16Unorm => 2, + DxgiFormat.DxgiFormatR16Unorm => 2, + DxgiFormat.DxgiFormatR16Uint => 2, + DxgiFormat.DxgiFormatR16Snorm => 2, + DxgiFormat.DxgiFormatR16Sint => 2, + DxgiFormat.DxgiFormatR8Typeless => 1, + DxgiFormat.DxgiFormatR8Unorm => 1, + DxgiFormat.DxgiFormatR8Uint => 1, + DxgiFormat.DxgiFormatR8Snorm => 1, + DxgiFormat.DxgiFormatR8Sint => 1, + DxgiFormat.DxgiFormatA8Unorm => 1, + DxgiFormat.DxgiFormatR1Unorm => 1, + DxgiFormat.DxgiFormatR9G9B9E5Sharedexp => 4, + DxgiFormat.DxgiFormatR8G8B8G8Unorm => 4, + DxgiFormat.DxgiFormatG8R8G8B8Unorm => 4, + DxgiFormat.DxgiFormatBc1Typeless => 8, + DxgiFormat.DxgiFormatBc1Unorm => 8, + DxgiFormat.DxgiFormatBc1UnormSrgb => 8, + DxgiFormat.DxgiFormatBc2Typeless => 16, + DxgiFormat.DxgiFormatBc2Unorm => 16, + DxgiFormat.DxgiFormatBc2UnormSrgb => 16, + DxgiFormat.DxgiFormatBc3Typeless => 16, + DxgiFormat.DxgiFormatBc3Unorm => 16, + DxgiFormat.DxgiFormatBc3UnormSrgb => 16, + DxgiFormat.DxgiFormatBc4Typeless => 8, + DxgiFormat.DxgiFormatBc4Unorm => 8, + DxgiFormat.DxgiFormatBc4Snorm => 8, + DxgiFormat.DxgiFormatBc5Typeless => 16, + DxgiFormat.DxgiFormatBc5Unorm => 16, + DxgiFormat.DxgiFormatBc5Snorm => 16, + DxgiFormat.DxgiFormatB5G6R5Unorm => 2, + DxgiFormat.DxgiFormatB5G5R5A1Unorm => 2, + DxgiFormat.DxgiFormatB8G8R8A8Unorm => 4, + DxgiFormat.DxgiFormatB8G8R8X8Unorm => 4, + DxgiFormat.DxgiFormatR10G10B10XrBiasA2Unorm => 4, + DxgiFormat.DxgiFormatB8G8R8A8Typeless => 4, + DxgiFormat.DxgiFormatB8G8R8A8UnormSrgb => 4, + DxgiFormat.DxgiFormatB8G8R8X8Typeless => 4, + DxgiFormat.DxgiFormatB8G8R8X8UnormSrgb => 4, + DxgiFormat.DxgiFormatBc6HTypeless => 16, + DxgiFormat.DxgiFormatBc6HUf16 => 16, + DxgiFormat.DxgiFormatBc6HSf16 => 16, + DxgiFormat.DxgiFormatBc7Typeless => 16, + DxgiFormat.DxgiFormatBc7Unorm => 16, + DxgiFormat.DxgiFormatBc7UnormSrgb => 16, + DxgiFormat.DxgiFormatP8 => 1, + DxgiFormat.DxgiFormatA8P8 => 2, + DxgiFormat.DxgiFormatB4G4R4A4Unorm => 2, + DxgiFormat.DxgiFormatAtcExt => 8, + DxgiFormat.DxgiFormatAtcExplicitAlphaExt => 16, + DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt => 16, + _ => 4, + }; + } + + public static bool IsCompressedFormat(this DxgiFormat format) + { + return format switch + { + DxgiFormat.DxgiFormatBc1Typeless or DxgiFormat.DxgiFormatBc1Unorm or DxgiFormat.DxgiFormatBc1UnormSrgb or DxgiFormat.DxgiFormatBc2Typeless or DxgiFormat.DxgiFormatBc2Unorm or DxgiFormat.DxgiFormatBc2UnormSrgb or DxgiFormat.DxgiFormatBc3Typeless or DxgiFormat.DxgiFormatBc3Unorm or DxgiFormat.DxgiFormatBc3UnormSrgb or DxgiFormat.DxgiFormatBc4Typeless or DxgiFormat.DxgiFormatBc4Unorm or DxgiFormat.DxgiFormatBc4Snorm or DxgiFormat.DxgiFormatBc5Typeless or DxgiFormat.DxgiFormatBc5Unorm or DxgiFormat.DxgiFormatBc5Snorm or DxgiFormat.DxgiFormatBc6HTypeless or DxgiFormat.DxgiFormatBc6HUf16 or DxgiFormat.DxgiFormatBc6HSf16 or DxgiFormat.DxgiFormatBc7Typeless or DxgiFormat.DxgiFormatBc7Unorm or DxgiFormat.DxgiFormatBc7UnormSrgb or DxgiFormat.DxgiFormatAtcExt or DxgiFormat.DxgiFormatAtcExplicitAlphaExt or DxgiFormat.DxgiFormatAtcInterpolatedAlphaExt => true, + _ => false, + }; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/ImageFile.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/ImageFile.cs new file mode 100644 index 0000000..4af950c --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/ImageFile.cs @@ -0,0 +1,74 @@ +using System.Text; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles +{ + /// + /// The format identifier of an image file. + /// + public enum ImageFileFormat + { + /// + /// Represents the KTX image file format. + /// + Ktx, + + /// + /// Represents the DDS image file format. + /// + Dds, + + /// + /// Represents an unknown image file format. + /// + Unknown + } + + /// + /// Static helper class to determine the format of an image file. + /// + public static class ImageFile + { + private static readonly byte[] ktx1Identifier = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + + /// + /// Determines the image file format of the given stream. + /// + /// The stream of data to identify. + /// The format this image file may contain. + public static ImageFileFormat DetermineImageFormat(Stream stream) + { + if (IsDds(stream)) + { + return ImageFileFormat.Dds; + } + + if (IsKtx(stream)) + { + return ImageFileFormat.Ktx; + } + + return ImageFileFormat.Unknown; + } + + private static bool IsDds(Stream stream) + { + using var br = new BinaryReader(stream, Encoding.UTF8, true); + + var magic = br.ReadUInt32(); + stream.Position -= 4; + + return magic == 0x20534444U; + } + + private static bool IsKtx(Stream stream) + { + // Only checks for version 1 + using var br = new BinaryReader(stream, Encoding.ASCII, true); + + var identifier = br.ReadBytes(12); + stream.Position -= 12; + + return identifier.SequenceEqual(ktx1Identifier); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/KtxFile.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/KtxFile.cs new file mode 100644 index 0000000..0d83a38 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageFiles/KtxFile.cs @@ -0,0 +1,371 @@ +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using static System.Text.Encoding; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles +{ + /// + /// A representation of a ktx file. + /// This class handles loading and saving ktx files into streams. + /// The full spec can be found here: https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ + /// + public class KtxFile + { + + public KtxHeader header; + public List KeyValuePairs { get; } = new List(); + public List MipMaps { get; } = new List(); + + public KtxFile() { } + public KtxFile(KtxHeader header) + { + this.header = header; + } + + /// + /// Writes this ktx file into a stream. + /// + public void Write(Stream s) + { + if (MipMaps.Count < 1 || MipMaps[0].NumberOfFaces < 1) + { + throw new InvalidOperationException("The KTX structure should have at least 1 mipmap level and 1 Face before writing to file."); + } + + using (var bw = new BinaryWriter(s, UTF8, true)) + { + var bytesOfKeyValueData = (uint)KeyValuePairs.Sum(x => x.GetSizeWithPadding()); + + header.BytesOfKeyValueData = bytesOfKeyValueData; + header.NumberOfFaces = MipMaps[0].NumberOfFaces; + header.NumberOfMipmapLevels = (uint)MipMaps.Count; + header.NumberOfArrayElements = 0; + + if (!header.VerifyHeader()) + { + throw new InvalidOperationException("Please verify the header validity before writing to file."); + } + + bw.WriteStruct(header); + + foreach (var keyValuePair in KeyValuePairs) + { + KtxKeyValuePair.WriteKeyValuePair(bw, keyValuePair); + } + + for (var mip = 0; mip < header.NumberOfMipmapLevels; mip++) + { + var imageSize = MipMaps[mip].SizeInBytes; + bw.Write(imageSize); + var isCubemap = header.NumberOfFaces == 6 && header.NumberOfArrayElements == 0; + for (var f = 0; f < header.NumberOfFaces; f++) + { + bw.Write(MipMaps[mip].Faces[f].Data); + var cubePadding = 0u; + if (isCubemap) + { + cubePadding = 3 - (imageSize + 3) % 4; + } + bw.AddPadding(cubePadding); + } + + var mipPaddingBytes = 3 - (imageSize + 3) % 4; + bw.AddPadding(mipPaddingBytes); + } + + } + } + + /// + /// Loads a KTX file from a stream. + /// + public static KtxFile Load(Stream s) + { + + using (var br = new BinaryReader(s, UTF8, true)) + { + var header = br.ReadStruct(); + + if (header.NumberOfArrayElements > 0) + { + throw new NotSupportedException("KTX files with arrays are not supported."); + } + + var ktx = new KtxFile(header); + + var keyValuePairBytesRead = 0; + while (keyValuePairBytesRead < header.BytesOfKeyValueData) + { + var kvp = KtxKeyValuePair.ReadKeyValuePair(br, out var read); + keyValuePairBytesRead += read; + ktx.KeyValuePairs.Add(kvp); + } + + var numberOfFaces = Math.Max(1, header.NumberOfFaces); + ktx.MipMaps.Capacity = (int)header.NumberOfMipmapLevels; + for (uint mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) + { + var imageSize = br.ReadUInt32(); + var mipWidth = header.PixelWidth / (uint)Math.Pow(2, mipLevel); + var mipHeight = header.PixelHeight / (uint)Math.Pow(2, mipLevel); + + ktx.MipMaps.Add(new KtxMipmap(imageSize, mipWidth, mipHeight, numberOfFaces)); + + var cubemap = header.NumberOfFaces > 1 && header.NumberOfArrayElements == 0; + for (uint face = 0; face < numberOfFaces; face++) + { + var faceData = br.ReadBytes((int)imageSize); + ktx.MipMaps[(int)mipLevel].Faces[(int)face] = new KtxMipFace(faceData, mipWidth, mipHeight); + if (cubemap) + { + var cubePadding = 0u; + cubePadding = 3 - (imageSize + 3) % 4; + br.SkipPadding(cubePadding); + } + } + + var mipPaddingBytes = 3 - (imageSize + 3) % 4; + br.SkipPadding(mipPaddingBytes); + } + + return ktx; + } + } + + /// + /// Gets the total size of all mipmaps and faces. + /// + public ulong GetTotalSize() + { + ulong totalSize = 0; + + for (var mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) + { + for (var face = 0; face < header.NumberOfFaces; face++) + { + var ktxface = MipMaps[mipLevel].Faces[face]; + totalSize += ktxface.SizeInBytes; + } + } + + return totalSize; + } + + /// + /// Gets all texture data of the file in face-major order (face0_mip0 ... face0_mip1 ... face1_mip0 ...) + /// + public byte[] GetAllTextureDataFaceMajor() + { + var result = new byte[GetTotalSize()]; + uint start = 0; + for (var face = 0; face < header.NumberOfFaces; face++) + { + for (var mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) + { + var ktxMipFace = MipMaps[mipLevel].Faces[face]; + ktxMipFace.Data.CopyTo(result, (int)start); + start += ktxMipFace.SizeInBytes; + } + } + + return result; + } + + /// + /// Gets all texture data of the file in MipMap-major order (face0_mip0 ... face1_mip0 ... face0_mip1 ...) + /// + public byte[] GetAllTextureDataMipMajor() + { + var result = new byte[GetTotalSize()]; + uint start = 0; + for (var mipLevel = 0; mipLevel < header.NumberOfMipmapLevels; mipLevel++) + { + for (var face = 0; face < header.NumberOfFaces; face++) + { + var ktxMipFace = MipMaps[mipLevel].Faces[face]; + ktxMipFace.Data.CopyTo(result, (int)start); + start += ktxMipFace.SizeInBytes; + } + } + + return result; + } + } + + public class KtxKeyValuePair + { + public string Key { get; } + public byte[] Value { get; } + public KtxKeyValuePair(string key, byte[] value) + { + Key = key; + Value = value; + } + + public uint GetSizeWithPadding() + { + var keySpanLength = UTF8.GetByteCount(Key); + var totalSize = (uint)(keySpanLength + 1 + Value.Length); + var paddingBytes = (int)(3 - (totalSize + 3) % 4); + + return (uint)(totalSize + paddingBytes); + } + + public static KtxKeyValuePair ReadKeyValuePair(BinaryReader br, out int bytesRead) + { + var totalSize = br.ReadUInt32(); + Span keyValueBytes = stackalloc byte[(int)totalSize]; + br.Read(keyValueBytes); + + // Find the key's null terminator + int i; + for (i = 0; i < totalSize; i++) + { + if (keyValueBytes[i] == 0) + { + break; + } + + if (i >= totalSize) + { + throw new InvalidDataException(); + } + } + + + var keySize = i; + var key = UTF8.GetString(keyValueBytes.Slice(0, keySize)); + + var valueSize = (int)(totalSize - keySize - 1); + var valueBytes = keyValueBytes.Slice(i + 1, valueSize); + var value = new byte[valueSize]; + valueBytes.CopyTo(value); + + var paddingBytes = (int)(3 - (totalSize + 3) % 4); + br.SkipPadding(paddingBytes); + + bytesRead = (int)(totalSize + paddingBytes + sizeof(uint)); + return new KtxKeyValuePair(key, value); + } + + public static uint WriteKeyValuePair(BinaryWriter bw, KtxKeyValuePair pair) + { + var keySpanLength = UTF8.GetByteCount(pair.Key); + Span keySpan = stackalloc byte[keySpanLength]; + Span valueSpan = pair.Value; + + var totalSize = (uint)(keySpan.Length + 1 + valueSpan.Length); + var paddingBytes = (int)(3 - (totalSize + 3) % 4); + + bw.Write(totalSize); + bw.Write(keySpan); + bw.Write((byte)0); + bw.Write(valueSpan); + + return (uint)(totalSize + paddingBytes); + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KtxHeader + { + public fixed byte Identifier[12]; + public uint Endianness; + public GlType GlType; + public uint GlTypeSize; + public GlFormat GlFormat; + public GlInternalFormat GlInternalFormat; + public GlFormat GlBaseInternalFormat; + public uint PixelWidth; + public uint PixelHeight; + + public uint PixelDepth; + public uint NumberOfArrayElements; + public uint NumberOfFaces; + public uint NumberOfMipmapLevels; + public uint BytesOfKeyValueData; + + public bool VerifyHeader() + { + Span id = stackalloc byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + for (var i = 0; i < id.Length; i++) + { + if (Identifier[i] != id[i]) return false; + } + return true; + } + + public static KtxHeader InitializeCompressed(int width, int height, GlInternalFormat internalFormat, GlFormat baseInternalFormat) + { + var header = new KtxHeader(); + Span id = stackalloc byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + for (var i = 0; i < id.Length; i++) + { + header.Identifier[i] = id[i]; + } + header.Endianness = 0x04030201; + header.PixelWidth = (uint)width; + header.PixelHeight = (uint)height; + header.GlType = 0; + header.GlTypeSize = 1; + header.GlFormat = 0; + header.GlInternalFormat = internalFormat; + header.GlBaseInternalFormat = baseInternalFormat; + + return header; + } + + public static KtxHeader InitializeUncompressed(int width, int height, GlType type, GlFormat format, uint glTypeSize, GlInternalFormat internalFormat, GlFormat baseInternalFormat) + { + var header = new KtxHeader(); + Span id = stackalloc byte[] { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + for (var i = 0; i < id.Length; i++) + { + header.Identifier[i] = id[i]; + } + header.Endianness = 0x04030201; + header.PixelWidth = (uint)width; + header.PixelHeight = (uint)height; + header.GlType = type; + header.GlTypeSize = glTypeSize; + header.GlFormat = format; + header.GlInternalFormat = internalFormat; + header.GlBaseInternalFormat = baseInternalFormat; + + return header; + } + } + + public class KtxMipFace + { + public uint Width { get; set; } + public uint Height { get; set; } + public uint SizeInBytes { get; } + public byte[] Data { get; } + + public KtxMipFace(byte[] data, uint width, uint height) + { + Width = width; + Height = height; + SizeInBytes = (uint)data.Length; + Data = data; + } + } + + public class KtxMipmap + { + public uint SizeInBytes { get; } + public uint Width { get; } + public uint Height { get; } + public uint NumberOfFaces { get; } + public KtxMipFace[] Faces { get; } + public KtxMipmap(uint sizeInBytes, uint width, uint height, uint numberOfFaces) + { + SizeInBytes = sizeInBytes; + Width = Math.Max(1, width); + Height = Math.Max(1, height); + NumberOfFaces = numberOfFaces; + Faces = new KtxMipFace[numberOfFaces]; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageQuality.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageQuality.cs new file mode 100644 index 0000000..6dc95e7 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageQuality.cs @@ -0,0 +1,101 @@ +using System; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public class ImageQuality + { + public static float PeakSignalToNoiseRatio(ReadOnlySpan original, ReadOnlySpan other, bool countAlpha = true) + { + if (original.Length != other.Length) + { + throw new ArgumentException("Both spans should be the same length"); + } + float error = 0; + for (var i = 0; i < original.Length; i++) + { + var o = new ColorYCbCr(original[i]); + var c = new ColorYCbCr(other[i]); + error += (o.y - c.y) * (o.y - c.y); + error += (o.cb - c.cb) * (o.cb - c.cb); + error += (o.cr - c.cr) * (o.cr - c.cr); + if (countAlpha) + { + error += (original[i].a - other[i].a) / 255.0f * ((original[i].a - other[i].a) / 255.0f); + } + + } + if (error < float.Epsilon) + { + return 100; + } + if (countAlpha) + { + error /= original.Length * 4; + } + else + { + error /= original.Length * 3; + } + + return 20 * MathF.Log10(1 / MathF.Sqrt(error)); + } + + public static float CalculateLogRMSE(ReadOnlySpan original, ReadOnlySpan other) + { + if (original.Length != other.Length) + { + throw new ArgumentException("Both spans should be the same length"); + } + float error = 0; + for (var i = 0; i < original.Length; i++) + { + //var dr = MathF.Log(other[i].r + 1.0f) - MathF.Log(original[i].r + 1.0f); + //var dg = MathF.Log(other[i].g + 1.0f) - MathF.Log(original[i].g + 1.0f); + //var db = MathF.Log(other[i].b + 1.0f) - MathF.Log(original[i].b + 1.0f); + var dr = Math.Sign(other[i].r) * MathF.Log(1 + MathF.Abs(other[i].r)) - Math.Sign(original[i].r) * MathF.Log(1 + MathF.Abs(original[i].r)); + var dg = Math.Sign(other[i].g) * MathF.Log(1 + MathF.Abs(other[i].g)) - Math.Sign(original[i].g) * MathF.Log(1 + MathF.Abs(original[i].g)); + var db = Math.Sign(other[i].b) * MathF.Log(1 + MathF.Abs(other[i].b)) - Math.Sign(original[i].b) * MathF.Log(1 + MathF.Abs(original[i].b)); + + error += dr * dr; + error += dg * dg; + error += db * db; + + } + return MathF.Sqrt(error / (3.0f * original.Length)); + } + + public static float PeakSignalToNoiseRatioLuminance(ReadOnlySpan original, ReadOnlySpan other, bool countAlpha = true) + { + if (original.Length != other.Length) + { + throw new ArgumentException("Both spans should be the same length"); + } + float error = 0; + for (var i = 0; i < original.Length; i++) + { + var o = new ColorYCbCr(original[i]); + var c = new ColorYCbCr(other[i]); + error += (o.y - c.y) * (o.y - c.y); + if (countAlpha) + { + error += (original[i].a - other[i].a) / 255.0f * ((original[i].a - other[i].a) / 255.0f); + } + + } + if (error < float.Epsilon) + { + return 100; + } + if (countAlpha) + { + error /= original.Length * 2; + } + else + { + error /= original.Length; + } + + return 20 * MathF.Log10(1 / MathF.Sqrt(error)); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageToBlocks.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageToBlocks.cs new file mode 100644 index 0000000..e807f12 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ImageToBlocks.cs @@ -0,0 +1,220 @@ +using CommunityToolkit.HighPerformance; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class ImageToBlocks + { + #region Blocks to colors + + internal static ColorRgba32[] ColorsFromRawBlocks(RawBlock4X4Rgba32[,] blocks, int pixelWidth, int pixelHeight) + { + var output = new ColorRgba32[pixelWidth * pixelHeight]; + + for (var y = 0; y < pixelHeight; y++) + { + for (var x = 0; x < pixelWidth; x++) + { + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + output[x + y * pixelWidth] = blocks[blockIndexX, blockIndexY][blockInternalIndexX, blockInternalIndexY]; + } + } + + return output; + } + + internal static ColorRgba32[] ColorsFromRawBlocks(RawBlock4X4Rgba32[] blocks, int pixelWidth, int pixelHeight) + { + var blocksWidth = (pixelWidth + 3 & ~3) >> 2; + var output = new ColorRgba32[pixelWidth * pixelHeight]; + + for (var y = 0; y < pixelHeight; y++) + { + for (var x = 0; x < pixelWidth; x++) + { + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + var blockIndex = blockIndexX + blockIndexY * blocksWidth; + + output[x + y * pixelWidth] = blocks[blockIndex][blockInternalIndexX, blockInternalIndexY]; + } + } + + return output; + } + + internal static ColorRgbFloat[] ColorsFromRawBlocks(RawBlock4X4RgbFloat[] blocks, int pixelWidth, int pixelHeight) + { + var blocksWidth = (pixelWidth + 3 & ~3) >> 2; + var output = new ColorRgbFloat[pixelWidth * pixelHeight]; + + for (var y = 0; y < pixelHeight; y++) + { + for (var x = 0; x < pixelWidth; x++) + { + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + var blockIndex = blockIndexX + blockIndexY * blocksWidth; + + output[x + y * pixelWidth] = blocks[blockIndex][blockInternalIndexX, blockInternalIndexY]; + } + } + + return output; + } + + #endregion + + #region Image to blocks + + internal static RawBlock4X4Rgba32[] ImageTo4X4(ReadOnlyMemory2D image, out int blocksWidth, out int blocksHeight) + { + blocksWidth = (image.Width + 3 & ~3) >> 2; + blocksHeight = (image.Height + 3 & ~3) >> 2; + + var output = new RawBlock4X4Rgba32[blocksWidth * blocksHeight]; + + var span = image.Span; + + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var color = span[y, x]; + + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + output[blockIndexX + blockIndexY * blocksWidth][blockInternalIndexX, blockInternalIndexY] = color; + } + } + + // Fill in block y with edge color + if ((image.Height & 3) != 0) + { + var yPaddingStart = image.Height & 3; + for (var i = 0; i < blocksWidth; i++) + { + var lastBlock = output[i + blocksWidth * (blocksHeight - 1)]; + for (var y = yPaddingStart; y < 4; y++) + { + for (var x = 0; x < 4; x++) + { + lastBlock[x, y] = lastBlock[x, y - 1]; + } + } + output[i + blocksWidth * (blocksHeight - 1)] = lastBlock; + } + } + + // Fill in block x with edge color + if ((image.Width & 3) != 0) + { + var xPaddingStart = image.Width & 3; + for (var i = 0; i < blocksHeight; i++) + { + var lastBlock = output[blocksWidth - 1 + i * blocksWidth]; + for (var x = xPaddingStart; x < 4; x++) + { + for (var y = 0; y < 4; y++) + { + lastBlock[x, y] = lastBlock[x - 1, y]; + } + } + output[blocksWidth - 1 + i * blocksWidth] = lastBlock; + } + } + + return output; + } + + internal static RawBlock4X4RgbFloat[] ImageTo4X4(ReadOnlyMemory2D image, out int blocksWidth, out int blocksHeight) + { + blocksWidth = (image.Width + 3 & ~3) >> 2; + blocksHeight = (image.Height + 3 & ~3) >> 2; + + var output = new RawBlock4X4RgbFloat[blocksWidth * blocksHeight]; + + var span = image.Span; + + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var color = span[y, x]; + + var blockIndexX = x >> 2; + var blockIndexY = y >> 2; + var blockInternalIndexX = x & 3; + var blockInternalIndexY = y & 3; + + output[blockIndexX + blockIndexY * blocksWidth][blockInternalIndexX, blockInternalIndexY] = color; + } + } + + // Fill in block y with edge color + if ((image.Height & 3) != 0) + { + var yPaddingStart = image.Height & 3; + for (var i = 0; i < blocksWidth; i++) + { + var lastBlock = output[i + blocksWidth * (blocksHeight - 1)]; + for (var y = yPaddingStart; y < 4; y++) + { + for (var x = 0; x < 4; x++) + { + lastBlock[x, y] = lastBlock[x, y - 1]; + } + } + output[i + blocksWidth * (blocksHeight - 1)] = lastBlock; + } + } + + // Fill in block x with edge color + if ((image.Width & 3) != 0) + { + var xPaddingStart = image.Width & 3; + for (var i = 0; i < blocksHeight; i++) + { + var lastBlock = output[blocksWidth - 1 + i * blocksWidth]; + for (var x = xPaddingStart; x < 4; x++) + { + for (var y = 0; y < 4; y++) + { + lastBlock[x, y] = lastBlock[x - 1, y]; + } + } + output[blocksWidth - 1 + i * blocksWidth] = lastBlock; + } + } + + return output; + } + + #endregion + + public static int CalculateNumOfBlocks(int pixelWidth, int pixelHeight) + { + var blocksWidth = (pixelWidth + 3 & ~3) >> 2; + var blocksHeight = (pixelHeight + 3 & ~3) >> 2; + + return blocksWidth * blocksHeight; + } + public static void CalculateNumOfBlocks(int pixelWidth, int pixelHeight, out int blocksWidth, out int blocksHeight) + { + blocksWidth = (pixelWidth + 3 & ~3) >> 2; + blocksHeight = (pixelHeight + 3 & ~3) >> 2; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/IntHelper.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/IntHelper.cs new file mode 100644 index 0000000..e021a7b --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/IntHelper.cs @@ -0,0 +1,19 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class IntHelper + { + + public static int SignExtend(int orig, int precision) + { + var signMask = 1 << precision - 1; + var numberMask = signMask - 1; + + if ((orig & signMask) != 0) + { + return ~numberMask | orig & numberMask; + } + + return orig & numberMask; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/InternalUtils.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/InternalUtils.cs new file mode 100644 index 0000000..e6b4b3f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/InternalUtils.cs @@ -0,0 +1,12 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class InternalUtils + { + public static void Swap(ref T lhs, ref T rhs) + { + var temp = lhs; + lhs = rhs; + rhs = temp; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Interpolation.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Interpolation.cs new file mode 100644 index 0000000..95a2df5 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/Interpolation.cs @@ -0,0 +1,103 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class Interpolation + { + /// + /// Interpolates two colors by half. + /// + /// The first color endpoint. + /// The second color endpoint. + /// The interpolated color. + public static ColorRgb24 InterpolateHalf(this ColorRgb24 c0, ColorRgb24 c1) => + InterpolateColor(c0, c1, 1, 2); + + /// + /// Interpolates two colors by third. + /// + /// The first color endpoint. + /// The second color endpoint. + /// The dividend in the third. + /// The interpolated color. + public static ColorRgb24 InterpolateThird(this ColorRgb24 c0, ColorRgb24 c1, int num) => + InterpolateColor(c0, c1, num, 3); + + /// + /// Interpolates two colors by fourth with ATC interpolation. + /// + /// The first color endpoint. + /// The second color endpoint. + /// The dividend in the fourth. + /// The interpolated color. + public static ColorRgb24 InterpolateFourthAtc(this ColorRgb24 c0, ColorRgb24 c1, int num) => + InterpolateColorAtc(c0, c1, num, 4); + + /// + /// Interpolates two colors by fifth. + /// + /// The first component. + /// The second component. + /// The dividend in the fifth. + /// The interpolated component. + public static byte InterpolateFifth(this byte a0, byte a1, int num) => + (byte)Interpolate(a0, a1, num, 5, 2); + + /// + /// Interpolates two colors by seventh. + /// + /// The first component. + /// The second component. + /// The dividend in the seventh. + /// The interpolated component. + public static byte InterpolateSeventh(this byte a0, byte a1, int num) => + (byte)Interpolate(a0, a1, num, 7, 3); + + /// + /// Interpolates two colors. + /// + /// The first color. + /// The second color. + /// The dividend on each color component. + /// The divisor on each color component. + /// The interpolated color. + private static ColorRgb24 InterpolateColor(ColorRgb24 c0, ColorRgb24 c1, int num, int den) => new ColorRgb24( + (byte)Interpolate(c0.r, c1.r, num, den), + (byte)Interpolate(c0.g, c1.g, num, den), + (byte)Interpolate(c0.b, c1.b, num, den)); + + /// + /// Interpolates two colors with the ATC interpolation. + /// + /// The first color. + /// The second color. + /// The dividend on each color component. + /// The divisor on each color component. + /// The interpolated color. + private static ColorRgb24 InterpolateColorAtc(ColorRgb24 c0, ColorRgb24 c1, int num, int den) => new ColorRgb24( + (byte)InterpolateAtc(c0.r, c1.r, num, den), + (byte)InterpolateAtc(c0.g, c1.g, num, den), + (byte)InterpolateAtc(c0.b, c1.b, num, den)); + + /// + /// Interpolates two components. + /// + /// The first component. + /// The second component. + /// The dividend. + /// The divisor. + /// A correction value for increased accuracy when working with integer interpolated values. + /// The interpolated component. + private static int Interpolate(int a, int b, int num, int den, int correction = 0) => + (int)(((den - num) * a + num * b + correction) / (float)den); + + /// + /// Interpolates two components with the ATC interpolation. + /// + /// The first component. + /// The second component. + /// The dividend. + /// The divisor. + /// The interpolated component. + private static int InterpolateAtc(int a, int b, int num, int den) => + (int)(a - num / (float)den * b); + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/LinearClustering.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/LinearClustering.cs new file mode 100644 index 0000000..ed2714d --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/LinearClustering.cs @@ -0,0 +1,510 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// Simple Linear Iterative Clustering. + /// + internal static class LinearClustering + { + + private struct LabXy + { + public float l; + public float a; + public float b; + public float x; + public float y; + + + public static LabXy operator +(LabXy left, LabXy right) + { + return new LabXy() + { + l = left.l + right.l, + a = left.a + right.a, + b = left.b + right.b, + x = left.x + right.x, + y = left.y + right.y, + }; + } + + public static LabXy operator /(LabXy left, int right) + { + return new LabXy() + { + l = left.l / right, + a = left.a / right, + b = left.b / right, + x = left.x / right, + y = left.y / right, + }; + } + } + + private struct ClusterCenter(LabXy labxy) + { + public float l = labxy.l; + public float a = labxy.a; + public float b = labxy.b; + public float x = labxy.x; + public float y = labxy.y; + public int count = 0; + + public readonly float Distance(LabXy other, float m, float s) + { + var dLab = MathF.Sqrt( + MathF.Pow(l - other.l, 2) + + MathF.Pow(a - other.a, 2) + + MathF.Pow(b - other.b, 2)); + + var dXy = MathF.Sqrt( + MathF.Pow(x - other.x, 2) + + MathF.Pow(y - other.y, 2)); + return dLab + m / s * dXy; + } + + public readonly float Distance(ClusterCenter other, float m, float s) + { + var dLab = MathF.Sqrt( + (l - other.l) * (l - other.l) + + (a - other.a) * (a - other.a) + + (b - other.b) * (b - other.b)); + + var dXy = MathF.Sqrt( + (x - other.x) * (x - other.x) + + (y - other.y) * (y - other.y)); + return dLab + m / s * dXy; + } + + public static ClusterCenter operator +(ClusterCenter left, LabXy right) + { + return new ClusterCenter() + { + l = left.l + right.l, + a = left.a + right.a, + b = left.b + right.b, + x = left.x + right.x, + y = left.y + right.y, + count = left.count + 1 + }; + } + + public static ClusterCenter operator /(ClusterCenter left, int right) + { + return new ClusterCenter() + { + l = left.l / right, + a = left.a / right, + b = left.b / right, + x = left.x / right, + y = left.y / right, + count = left.count + }; + } + } + + /// + /// The greater the value of M, + /// the more spatial proximity is emphasized and the more compact the cluster, + /// M should be in range of 1 to 20. + /// + public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int height, + int clusters, float m = 10, int maxIterations = 10, bool enforceConnectivity = true) + { + + var floats = new ColorRgbFloat[pixels.Length]; + for (var i = 0; i < pixels.Length; i++) + { + floats[i] = pixels[i].ToRgbFloat(); + } + + return ClusterPixels(floats, width, height, clusters, m, maxIterations, enforceConnectivity); + + //Grid interval S + /* + var s = MathF.Sqrt(pixels.Length / (float)clusters); + var clusterIndices = new int[pixels.Length]; + + var labXys = ConvertToLabXy(pixels, width, height); + + Span clusterCenters = InitialClusterCenters(width, height, clusters, s, labXys); + Span previousCenters = new ClusterCenter[clusters]; + + float error = 999; + const float threshold = 0.1f; + var iter = 0; + while (error > threshold) + { + if (maxIterations > 0 && iter >= maxIterations) + { + break; + } + iter++; + + clusterCenters.CopyTo(previousCenters); + + Array.Fill(clusterIndices, -1); + + // Find closest cluster for pixels + for (var j = 0; j < clusters; j++) + { + var xL = Math.Max(0, (int)(clusterCenters[j].x - s)); + var xH = Math.Min(width, (int)(clusterCenters[j].x + s)); + var yL = Math.Max(0, (int)(clusterCenters[j].y - s)); + var yH = Math.Min(height, (int)(clusterCenters[j].y + s)); + + for (var x = xL; x < xH; x++) + { + for (var y = yL; y < yH; y++) + { + var i = x + y * width; + + if (clusterIndices[i] == -1) + { + clusterIndices[i] = j; + } + else + { + var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, s); + var distance = clusterCenters[j].Distance(labXys[i], m, s); + if (distance < prevDistance) + { + clusterIndices[i] = j; + } + } + } + } + } + + error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, s, ref clusterCenters); + } + + if (enforceConnectivity) { + clusterIndices = EnforceConnectivity(clusterIndices, width, height, clusters); + } + + return clusterIndices; + */ + } + + /// + /// The greater the value of M, + /// the more spatial proximity is emphasized and the more compact the cluster, + /// M should be in range of 1 to 20. + /// + public static int[] ClusterPixels(ReadOnlySpan pixels, int width, int height, + int clusters, float m = 10, int maxIterations = 10, bool enforceConnectivity = true) + { + + if (clusters < 2) + { + throw new ArgumentException("Number of clusters should be more than 1"); + } + + //Grid interval S + var s = MathF.Sqrt(pixels.Length / (float)clusters); + var clusterIndices = new int[pixels.Length]; + + var labXys = ConvertToLabXy(pixels, width, height); + + + Span clusterCenters = InitialClusterCenters(width, height, clusters, s, labXys); + Span previousCenters = new ClusterCenter[clusters]; + + float error = 999; + const float threshold = 0.1f; + var iter = 0; + while (error > threshold) + { + if (maxIterations > 0 && iter >= maxIterations) + { + break; + } + iter++; + + clusterCenters.CopyTo(previousCenters); + + Array.Fill(clusterIndices, -1); + + // Find closest cluster for pixels + for (var j = 0; j < clusters; j++) + { + var xL = Math.Max(0, (int)(clusterCenters[j].x - s)); + var xH = Math.Min(width, (int)(clusterCenters[j].x + s)); + var yL = Math.Max(0, (int)(clusterCenters[j].y - s)); + var yH = Math.Min(height, (int)(clusterCenters[j].y + s)); + + for (var x = xL; x < xH; x++) + { + for (var y = yL; y < yH; y++) + { + var i = x + y * width; + + if (clusterIndices[i] == -1) + { + clusterIndices[i] = j; + } + else + { + var prevDistance = clusterCenters[clusterIndices[i]].Distance(labXys[i], m, s); + var distance = clusterCenters[j].Distance(labXys[i], m, s); + if (distance < prevDistance) + { + clusterIndices[i] = j; + } + } + } + } + } + + error = RecalculateCenters(clusters, m, labXys, clusterIndices, previousCenters, s, ref clusterCenters); + } + + if (enforceConnectivity) + { + clusterIndices = EnforceConnectivity(clusterIndices, width, height, clusters); + } + + return clusterIndices; + } + + private static float RecalculateCenters(int clusters, float m, LabXy[] labXys, int[] clusterIndices, + Span previousCenters, float s, ref Span clusterCenters) + { + clusterCenters.Clear(); + for (var i = 0; i < labXys.Length; i++) + { + var clusterIndex = clusterIndices[i]; + // Sometimes a pixel is out of the range of any cluster, + // in that case, find the nearest cluster and add it to it + if (clusterIndex == -1) + { + var bestCluster = 0; + var bestDistance = previousCenters[0].Distance(labXys[i], m, s); + for (var j = 1; j < clusters; j++) + { + var dist = previousCenters[j].Distance(labXys[i], m, s); + if (dist < bestDistance) + { + bestDistance = dist; + bestCluster = j; + } + } + clusterCenters[bestCluster] += labXys[i]; + clusterIndices[i] = bestCluster; + } + else + { + clusterCenters[clusterIndex] += labXys[i]; + } + } + + float error = 0; + for (var i = 0; i < clusters; i++) + { + if (clusterCenters[i].count > 0) + { + clusterCenters[i] /= clusterCenters[i].count; + error += clusterCenters[i].Distance(previousCenters[i], m, s); + } + } + + error /= clusters; + return error; + } + + private static ClusterCenter[] InitialClusterCenters(int width, int height, int clusters, float s, LabXy[] labXys) + { + var clusterCenters = new ClusterCenter[clusters]; + + if (clusters == 2) + { + var x0 = (int)MathF.Floor(width * 0.333f); + var y0 = (int)MathF.Floor(height * 0.333f); + + var x1 = (int)MathF.Floor(width * 0.666f); + var y1 = (int)MathF.Floor(height * 0.666f); + + var i0 = x0 + y0 * width; + clusterCenters[0] = new ClusterCenter(labXys[i0]); + + var i1 = x1 + y1 * width; + clusterCenters[1] = new ClusterCenter(labXys[i1]); + } + else if (clusters == 3) + { + var x0 = (int)MathF.Floor(width * 0.333f); + var y0 = (int)MathF.Floor(height * 0.333f); + var i0 = x0 + y0 * width; + clusterCenters[0] = new ClusterCenter(labXys[i0]); + + var x1 = (int)MathF.Floor(width * 0.666f); + var y1 = (int)MathF.Floor(height * 0.333f); + var i1 = x1 + y1 * width; + clusterCenters[1] = new ClusterCenter(labXys[i1]); + + var x2 = (int)MathF.Floor(width * 0.5f); + var y2 = (int)MathF.Floor(height * 0.666f); + var i2 = x2 + y2 * width; + clusterCenters[2] = new ClusterCenter(labXys[i2]); + } + else + { + var cIdx = 0; + //Choose initial centers + for (var x = s / 2; x < width; x += s) + { + for (var y = s / 2; y < height; y += s) + { + if (cIdx >= clusterCenters.Length) + { + break; + } + + var i = (int)x + (int)y * width; + clusterCenters[cIdx] = new ClusterCenter(labXys[i]); + cIdx++; + } + } + } + return clusterCenters; + } + + /* + private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int width, int height) + { + var labXys = new LabXy[pixels.Length]; + //Convert pixels to LabXy + for (var x = 0; x < width; x++) + { + for (var y = 0; y < height; y++) + { + var i = x + y * width; + var lab = new ColorLab(pixels[i]); + labXys[i] = new LabXy + { + l = lab.l, + a = lab.a, + b = lab.b, + x = x, + y = y + }; + } + } + + return labXys; + } + */ + + private static LabXy[] ConvertToLabXy(ReadOnlySpan pixels, int width, int height) + { + var labXys = new LabXy[pixels.Length]; + //Convert pixels to LabXy + for (var x = 0; x < width; x++) + { + for (var y = 0; y < height; y++) + { + var i = x + y * width; + var lab = new ColorLab(pixels[i]); + labXys[i] = new LabXy + { + l = lab.l, + a = lab.a, + b = lab.b, + x = x, + y = y + }; + } + } + + return labXys; + } + + private static int[] EnforceConnectivity(int[] oldLabels, int width, int height, int clusters) + { + ReadOnlySpan neighborX = new[] { -1, 0, 1, 0 }; + ReadOnlySpan neighborY = new[] { 0, -1, 0, 1 }; + + var sSquared = width * height / clusters; + + var clusterX = new List(sSquared); + var clusterY = new List(sSquared); + + var adjacentLabel = 0; + var newLabels = new int[oldLabels.Length]; + var usedLabels = new bool[clusters]; + Array.Fill(newLabels, -1); + + for (var y = 0; y < height; ++y) + { + for (var x = 0; x < width; ++x) + { + var xyIndex = x + y * width; + if (newLabels[xyIndex] < 0) + { + var label = oldLabels[xyIndex]; + newLabels[xyIndex] = label; + + //New cluster + clusterX.Add(x); + clusterY.Add(y); + + //Search neighbors for already completed clusters + for (var i = 0; i < neighborX.Length; ++i) + { + var nX = x + neighborX[i]; + var nY = y + neighborY[i]; + var nI = nX + nY * width; + if (nX < width && nX >= 0 && nY < height && nY >= 0) + { + if (newLabels[nI] >= 0) + { + adjacentLabel = newLabels[nI]; + break; + } + } + } + + //Count pixels in this cluster + for (var c = 0; c < clusterX.Count; ++c) + { + for (var i = 0; i < neighborX.Length; ++i) + { + var nX = clusterX[c] + neighborX[i]; + var nY = clusterY[c] + neighborY[i]; + var nI = nX + nY * width; + if (nX < width && nX >= 0 && nY < height && nY >= 0) + { + if (newLabels[nI] == -1 && label == oldLabels[nI]) + { + clusterX.Add(nX); + clusterY.Add(nY); + newLabels[nI] = label; + } + } + } + } + + // If this is unusually small cluster or this label is already used, + // merge with adjacent cluster + if (clusterX.Count < sSquared / 4 || usedLabels[label]) + { + for (var i = 0; i < clusterX.Count; ++i) + { + newLabels[clusterY[i] * width + clusterX[i]] = adjacentLabel; + } + } + else + { + usedLabels[label] = true; + } + + clusterX.Clear(); + clusterY.Clear(); + } + } + } + + return newLabels; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/MathHelper.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/MathHelper.cs new file mode 100644 index 0000000..550bfae --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/MathHelper.cs @@ -0,0 +1,61 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public static unsafe class MathHelper + { + private static double two54 = 1.80143985094819840000e+16; /* 0x43500000, 0x00000000 */ + + // FrExp modified to C# from http://www.netlib.org + /* @(#)fdlibm.h 1.5 04/04/22 */ + /* + * ==================================================== + * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved. + * + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ==================================================== + */ + /// + /// Breaks down the floating-point value x into a component m for the normalized fraction component and another term n for the exponent, such that the absolute value of m is greater than or equal to 0.5 and less than 1.0 or equal to 0, and x = m * 2n. The function stores the integer exponent n at the location to which expptr points. + /// + /// + /// + /// Returns the normalized fraction m. If x is 0, the function returns 0 for both the fraction and exponent. The fraction has the same sign as the argument x. The result of the function cannot have a range error. + public static double FrExp(double x, out int eptr) + { + unchecked + { + int hx, ix, lx; + hx = *(1 + (int*)&x); + ix = 0x7fffffff & hx; + lx = *(int*)&x; + eptr = 0; + if (ix >= 0x7ff00000 || (ix | lx) == 0) return x; /* 0,inf,nan */ + if (ix < 0x00100000) + { + /* subnormal */ + x *= two54; + hx = *(1 + (int*)&x); + ix = hx & 0x7fffffff; + eptr = -54; + } + + eptr += (ix >> 20) - 1022; + hx = hx & (int)0x800fffff | 0x3fe00000; + *(1 + (int*)&x) = hx; + return x; + } + } + + /// + /// Multiplies a floating point value arg by the number 2 raised to the exp power. + /// + /// + /// + /// + public static float LdExp(float arg, int exp) + { + return arg * MathF.Pow(2, exp); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/MipMapper.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/MipMapper.cs new file mode 100644 index 0000000..8f2af72 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/MipMapper.cs @@ -0,0 +1,224 @@ +using CommunityToolkit.HighPerformance; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class MipMapper + { + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The original image width. + /// The original image height. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory input, int width, int height, ref int numMipMaps) + { + return GenerateMipChain(input.AsMemory2D(height, width), ref numMipMaps); + } + + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory2D pixels, ref int numMipMaps) + { + var width = pixels.Width; + var height = pixels.Height; + var mipChainLength = CalculateMipChainLength(width, height, numMipMaps); + + var result = new ReadOnlyMemory2D[mipChainLength]; + result[0] = pixels; + + // If only one mipmap was requested, return original image only + if (numMipMaps == 1) + { + return result; + } + + // If number of mipmaps is "marked as boundless", do as many mipmaps as it takes to reach a size of 1x1 + if (numMipMaps <= 0) + { + numMipMaps = int.MaxValue; + } + + // Generate mipmaps + for (var mipLevel = 1; mipLevel < numMipMaps; mipLevel++) + { + var mipWidth = Math.Max(1, width >> mipLevel); + var mipHeight = Math.Max(1, height >> mipLevel); + + var newMip = ResizeToHalf(result[mipLevel - 1].Span); + result[mipLevel] = newMip; + + // Stop generating if last generated mipmap was of size 1x1 + if (mipWidth == 1 && mipHeight == 1) + { + numMipMaps = mipLevel + 1; + break; + } + } + + return result; + } + + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The original image width. + /// The original image height. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory input, int width, int height, ref int numMipMaps) + { + return GenerateMipChain(input.AsMemory2D(height, width), ref numMipMaps); + } + + /// + /// Generate a chain of elements. + /// + /// The original image to scale down. + /// The number of mipmaps to generate. + /// Will generate as many mipmaps as possible until a mipmap of 1x1 is reached for 0 or smaller. + public static ReadOnlyMemory2D[] GenerateMipChain(ReadOnlyMemory2D pixels, ref int numMipMaps) + { + var width = pixels.Width; + var height = pixels.Height; + var mipChainLength = CalculateMipChainLength(width, height, numMipMaps); + + var result = new ReadOnlyMemory2D[mipChainLength]; + result[0] = pixels; + + // If only one mipmap was requested, return original image only + if (numMipMaps == 1) + { + return result; + } + + // If number of mipmaps is "marked as boundless", do as many mipmaps as it takes to reach a size of 1x1 + if (numMipMaps <= 0) + { + numMipMaps = int.MaxValue; + } + + // Generate mipmaps + for (var mipLevel = 1; mipLevel < numMipMaps; mipLevel++) + { + var mipWidth = Math.Max(1, width >> mipLevel); + var mipHeight = Math.Max(1, height >> mipLevel); + + var newMip = ResizeToHalf(result[mipLevel - 1].Span); + result[mipLevel] = newMip; + + // Stop generating if last generated mipmap was of size 1x1 + if (mipWidth == 1 && mipHeight == 1) + { + numMipMaps = mipLevel + 1; + break; + } + } + + return result; + } + + public static int CalculateMipChainLength(int width, int height, int maxNumMipMaps) + { + if (maxNumMipMaps == 1) + { + return 1; + } + + if (maxNumMipMaps <= 0) + { + maxNumMipMaps = int.MaxValue; + } + + var output = 0; + for (var mipLevel = 1; mipLevel <= maxNumMipMaps; mipLevel++) + { + var mipWidth = Math.Max(1, width >> mipLevel); + var mipHeight = Math.Max(1, height >> mipLevel); + + if (mipLevel == maxNumMipMaps) + { + return maxNumMipMaps; + } + + if (mipWidth == 1 && mipHeight == 1) + { + output = mipLevel + 1; + break; + } + } + + return output; + } + + public static void CalculateMipLevelSize(int width, int height, int mipIdx, out int mipWidth, out int mipHeight) + { + mipWidth = Math.Max(1, width >> mipIdx); + mipHeight = Math.Max(1, height >> mipIdx); + } + + private static ColorRgba32[,] ResizeToHalf(ReadOnlySpan2D pixelsRgba) + { + + var oldWidth = pixelsRgba.Width; + var oldHeight = pixelsRgba.Height; + var newWidth = Math.Max(1, oldWidth >> 1); + var newHeight = Math.Max(1, oldHeight >> 1); + + var result = new ColorRgba32[newHeight, newWidth]; + + int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); + int ClampH(int y) => Math.Max(0, Math.Min(oldHeight - 1, y)); + + for (var y2 = 0; y2 < newHeight; y2++) + { + for (var x2 = 0; x2 < newWidth; x2++) + { + var ul = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2)].ToFloat(); + var ur = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2 + 1)].ToFloat(); + var ll = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2)].ToFloat(); + var lr = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2 + 1)].ToFloat(); + + result[y2, x2] = ((ul + ur + ll + lr) / 4).ToRgba32(); + } + } + + return result; + } + + private static ColorRgbFloat[,] ResizeToHalf(ReadOnlySpan2D pixelsRgba) + { + + var oldWidth = pixelsRgba.Width; + var oldHeight = pixelsRgba.Height; + var newWidth = Math.Max(1, oldWidth >> 1); + var newHeight = Math.Max(1, oldHeight >> 1); + + var result = new ColorRgbFloat[newHeight, newWidth]; + + int ClampW(int x) => Math.Max(0, Math.Min(oldWidth - 1, x)); + int ClampH(int y) => Math.Max(0, Math.Min(oldHeight - 1, y)); + + for (var y2 = 0; y2 < newHeight; y2++) + { + for (var x2 = 0; x2 < newWidth; x2++) + { + var ul = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2)]; + var ur = pixelsRgba[ClampH(y2 * 2), ClampW(x2 * 2 + 1)]; + var ll = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2)]; + var lr = pixelsRgba[ClampH(y2 * 2 + 1), ClampW(x2 * 2 + 1)]; + + result[y2, x2] = (ul + ur + ll + lr) / 4; + } + } + + return result; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OperationContext.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OperationContext.cs new file mode 100644 index 0000000..8fbce78 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OperationContext.cs @@ -0,0 +1,28 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// The operation context. + /// + public class OperationContext + { + /// + /// Whether the blocks should be decoded in parallel. + /// + public bool IsParallel { get; set; } + + /// + /// Determines how many tasks should be used for parallel processing. + /// + public int TaskCount { get; set; } = Environment.ProcessorCount; + + /// + /// The cancellation token to check if the asynchronous operation was cancelled. + /// + public CancellationToken CancellationToken { get; set; } + + /// + /// The progress context for the operation. + /// + public OperationProgress Progress { get; set; } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OperationProgress.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OperationProgress.cs new file mode 100644 index 0000000..82d8a2e --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OperationProgress.cs @@ -0,0 +1,25 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public class OperationProgress + { + private readonly IProgress progress; + private readonly int totalBlocks; + private int processedBlocks; + + public OperationProgress(IProgress progress, int totalBlocks) + { + this.progress = progress; + this.totalBlocks = totalBlocks; + } + + public void SetProcessedBlocks(int processedBlocks) + { + this.processedBlocks = processedBlocks; + } + + public void Report(int currentBlock) + { + progress?.Report(new ProgressElement(processedBlocks + currentBlock, totalBlocks)); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OutputFileFormat.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OutputFileFormat.cs new file mode 100644 index 0000000..a62544a --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/OutputFileFormat.cs @@ -0,0 +1,14 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public enum OutputFileFormat + { + /// + /// Khronos texture Format https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ + /// + Ktx, + /// + /// Direct draw surface Format https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds + /// + Dds + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/PcaVectors.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/PcaVectors.cs new file mode 100644 index 0000000..bc671ca --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/PcaVectors.cs @@ -0,0 +1,322 @@ +using System.Numerics; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + internal static class PcaVectors + { + private const int C565_5Mask = 0xF8; + private const int C565_6Mask = 0xFC; + + private static void ConvertToVector4(ReadOnlySpan colors, Span vectors) + { + for (var i = 0; i < colors.Length; i++) + { + vectors[i].X += colors[i].r / 255f; + vectors[i].Y += colors[i].g / 255f; + vectors[i].Z += colors[i].b / 255f; + vectors[i].W += colors[i].a / 255f; + } + } + + private static void ConvertToVector4(ReadOnlySpan colors, Span vectors) + { + for (var i = 0; i < colors.Length; i++) + { + vectors[i].X += colors[i].r; + vectors[i].Y += colors[i].g; + vectors[i].Z += colors[i].b; + vectors[i].W = 0; + } + } + + private static Vector4 CalculateMean(Span colors) + { + + float r = 0; + float g = 0; + float b = 0; + float a = 0; + + for (var i = 0; i < colors.Length; i++) + { + r += colors[i].X; + g += colors[i].Y; + b += colors[i].Z; + a += colors[i].W; + } + + return new Vector4( + r / colors.Length, + g / colors.Length, + b / colors.Length, + a / colors.Length + ); + } + + internal static Matrix4x4 CalculateCovariance(Span values, out Vector4 mean) + { + mean = CalculateMean(values); + for (var i = 0; i < values.Length; i++) + { + values[i] -= mean; + } + + //4x4 matrix + var mat = new Matrix4x4(); + + for (var i = 0; i < values.Length; i++) + { + mat.M11 += values[i].X * values[i].X; + mat.M12 += values[i].X * values[i].Y; + mat.M13 += values[i].X * values[i].Z; + mat.M14 += values[i].X * values[i].W; + + mat.M22 += values[i].Y * values[i].Y; + mat.M23 += values[i].Y * values[i].Z; + mat.M24 += values[i].Y * values[i].W; + + mat.M33 += values[i].Z * values[i].Z; + mat.M34 += values[i].Z * values[i].W; + + mat.M44 += values[i].W * values[i].W; + } + + mat = Matrix4x4.Multiply(mat, 1f / (values.Length - 1)); + + mat.M21 = mat.M12; + mat.M31 = mat.M13; + mat.M32 = mat.M23; + mat.M41 = mat.M14; + mat.M42 = mat.M24; + mat.M43 = mat.M34; + + return mat; + } + + /// + /// Calculate principal axis with the power-method + /// + /// + /// + internal static Vector4 CalculatePrincipalAxis(Matrix4x4 covarianceMatrix) + { + var lastDa = Vector4.UnitY; + + for (var i = 0; i < 30; i++) + { + var dA = Vector4.Transform(lastDa, covarianceMatrix); + + if (dA.LengthSquared() == 0) + { + break; + } + + dA = Vector4.Normalize(dA); + if (Vector4.Dot(lastDa, dA) > 0.999999) + { + lastDa = dA; + break; + } + else + { + lastDa = dA; + } + } + return lastDa; + } + + public static void Create(Span colors, out Vector3 mean, out Vector3 principalAxis) + { + Span vectors = stackalloc Vector4[colors.Length]; + ConvertToVector4(colors, vectors); + + + var cov = CalculateCovariance(vectors, out var v4Mean); + mean = new Vector3(v4Mean.X, v4Mean.Y, v4Mean.Z); + + var pa = CalculatePrincipalAxis(cov); + principalAxis = new Vector3(pa.X, pa.Y, pa.Z); + if (principalAxis.LengthSquared() == 0) + { + principalAxis = Vector3.UnitY; + } + else + { + principalAxis = Vector3.Normalize(principalAxis); + } + + } + + public static void Create(Span colors, out Vector3 mean, out Vector3 principalAxis) + { + Span vectors = stackalloc Vector4[colors.Length]; + ConvertToVector4(colors, vectors); + + + var cov = CalculateCovariance(vectors, out var v4Mean); + mean = new Vector3(v4Mean.X, v4Mean.Y, v4Mean.Z); + + var pa = CalculatePrincipalAxis(cov); + principalAxis = new Vector3(pa.X, pa.Y, pa.Z); + if (principalAxis.LengthSquared() == 0) + { + principalAxis = Vector3.UnitY; + } + else + { + principalAxis = Vector3.Normalize(principalAxis); + } + + } + + public static void CreateWithAlpha(Span colors, out Vector4 mean, out Vector4 principalAxis) + { + Span vectors = stackalloc Vector4[colors.Length]; + ConvertToVector4(colors, vectors); + + var cov = CalculateCovariance(vectors, out mean); + principalAxis = CalculatePrincipalAxis(cov); + } + + + public static void GetExtremePoints(Span colors, Vector3 mean, Vector3 principalAxis, out ColorRgb24 min, + out ColorRgb24 max) + { + + float minD = 0; + float maxD = 0; + + for (var i = 0; i < colors.Length; i++) + { + var colorVec = new Vector3(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f); + + var v = colorVec - mean; + var d = Vector3.Dot(v, principalAxis); + if (d < minD) minD = d; + if (d > maxD) maxD = d; + } + + var minVec = mean + principalAxis * minD; + var maxVec = mean + principalAxis * maxD; + + var minR = (int)(minVec.X * 255); + var minG = (int)(minVec.Y * 255); + var minB = (int)(minVec.Z * 255); + + var maxR = (int)(maxVec.X * 255); + var maxG = (int)(maxVec.Y * 255); + var maxB = (int)(maxVec.Z * 255); + + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; + + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; + + min = new ColorRgb24((byte)minR, (byte)minG, (byte)minB); + max = new ColorRgb24((byte)maxR, (byte)maxG, (byte)maxB); + } + + public static void GetMinMaxColor565(Span colors, Vector3 mean, Vector3 principalAxis, + out ColorRgb565 min, out ColorRgb565 max) + { + + float minD = 0; + float maxD = 0; + + for (var i = 0; i < colors.Length; i++) + { + var colorVec = new Vector3(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f); + + var v = colorVec - mean; + var d = Vector3.Dot(v, principalAxis); + if (d < minD) minD = d; + if (d > maxD) maxD = d; + } + + //Inset + minD *= 15 / 16f; + maxD *= 15 / 16f; + + var minVec = mean + principalAxis * minD; + var maxVec = mean + principalAxis * maxD; + + var minR = (int)(minVec.X * 255); + var minG = (int)(minVec.Y * 255); + var minB = (int)(minVec.Z * 255); + + var maxR = (int)(maxVec.X * 255); + var maxG = (int)(maxVec.Y * 255); + var maxB = (int)(maxVec.Z * 255); + + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; + + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; + + // Optimal round + minR = minR & C565_5Mask | minR >> 5; + minG = minG & C565_6Mask | minG >> 6; + minB = minB & C565_5Mask | minB >> 5; + + maxR = maxR & C565_5Mask | maxR >> 5; + maxG = maxG & C565_6Mask | maxG >> 6; + maxB = maxB & C565_5Mask | maxB >> 5; + + min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); + max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); + + } + + public static void GetExtremePointsWithAlpha(Span colors, Vector4 mean, Vector4 principalAxis, out Vector4 min, + out Vector4 max) + { + + float minD = 0; + float maxD = 0; + + for (var i = 0; i < colors.Length; i++) + { + var colorVec = new Vector4(colors[i].r / 255f, colors[i].g / 255f, colors[i].b / 255f, colors[i].a / 255f); + + var v = colorVec - mean; + var d = Vector4.Dot(v, principalAxis); + if (d < minD) minD = d; + if (d > maxD) maxD = d; + } + + min = mean + principalAxis * minD; + max = mean + principalAxis * maxD; + } + + public static void GetExtremePoints(Span colors, Vector3 mean, Vector3 principalAxis, out Vector3 min, + out Vector3 max) + { + + float minD = 0; + float maxD = 0; + + for (var i = 0; i < colors.Length; i++) + { + var colorVec = new Vector3(colors[i].r, colors[i].g, colors[i].b); + + var v = colorVec - mean; + var d = Vector3.Dot(v, principalAxis); + if (d < minD) minD = d; + if (d > maxD) maxD = d; + } + + minD *= 15 / 16f; + maxD *= 15 / 16f; + + min = mean + principalAxis * minD; + max = mean + principalAxis * maxD; + } + + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ProgressElement.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ProgressElement.cs new file mode 100644 index 0000000..4dbad65 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/ProgressElement.cs @@ -0,0 +1,31 @@ +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + public struct ProgressElement + { + /// + /// Current block being processed + /// + public int CurrentBlock { get; } + + /// + /// The total amount of blocks to be processed + /// + public int TotalBlocks { get; } + + /// + /// Returns the progress percentage as a float from 0 to 1 + /// + public readonly float Percentage => CurrentBlock / (float)TotalBlocks; + + public ProgressElement(int currentBlock, int totalBlocks) + { + CurrentBlock = currentBlock; + TotalBlocks = totalBlocks; + } + + public override readonly string ToString() + { + return $"{nameof(CurrentBlock)}: {CurrentBlock}, {nameof(TotalBlocks)}: {TotalBlocks}, {nameof(Percentage)}: {Percentage}"; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/RawBlocks.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/RawBlocks.cs new file mode 100644 index 0000000..290001e --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/RawBlocks.cs @@ -0,0 +1,319 @@ +using System.Runtime.InteropServices; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Encoder.Bptc; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + + public struct RawBlock4X4Rgba32 + { + public ColorRgba32 p00, p10, p20, p30; + public ColorRgba32 p01, p11, p21, p31; + public ColorRgba32 p02, p12, p22, p32; + public ColorRgba32 p03, p13, p23, p33; + + public RawBlock4X4Rgba32(ColorRgba32 fillColor) + { + p00 = p01 = p02 = p03 = + p10 = p11 = p12 = p13 = + p20 = p21 = p22 = p23 = + p30 = p31 = p32 = p33 = fillColor; + } + + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public ColorRgba32 this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public ColorRgba32 this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + internal int CalculateError(RawBlock4X4Rgba32 other, bool useAlpha = false) + { + float error = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = pix1[i]; + var col2 = pix2[i]; + + var re = col1.r - col2.r; + var ge = col1.g - col2.g; + var be = col1.b - col2.b; + + error += re * re; + error += ge * ge; + error += be * be; + + if (useAlpha) + { + var ae = col1.a - col2.a; + error += ae * ae * 4; + } + } + + error /= pix1.Length; + error = MathF.Sqrt(error); + + return (int)error; + } + + internal float CalculateYCbCrError(RawBlock4X4Rgba32 other) + { + float yError = 0; + float cbError = 0; + float crError = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = new ColorYCbCr(pix1[i]); + var col2 = new ColorYCbCr(pix2[i]); + + var ye = col1.y - col2.y; + var cbe = col1.cb - col2.cb; + var cre = col1.cr - col2.cr; + + yError += ye * ye; + cbError += cbe * cbe; + crError += cre * cre; + } + + var error = yError * 2 + cbError / 2 + crError / 2; + + return error; + } + + internal float CalculateYCbCrAlphaError(RawBlock4X4Rgba32 other, float yMultiplier = 2, float alphaMultiplier = 1) + { + float yError = 0; + float cbError = 0; + float crError = 0; + float alphaError = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = new ColorYCbCrAlpha(pix1[i]); + var col2 = new ColorYCbCrAlpha(pix2[i]); + + var ye = (col1.y - col2.y) * yMultiplier; + var cbe = col1.cb - col2.cb; + var cre = col1.cr - col2.cr; + var ae = (col1.alpha - col2.alpha) * alphaMultiplier; + + yError += ye * ye; + cbError += cbe * cbe; + crError += cre * cre; + alphaError += ae * ae; + } + + var error = yError + cbError + crError + alphaError; + return error; + } + + internal RawBlock4X4Ycbcr ToRawBlockYcbcr() + { + var rawYcbcr = new RawBlock4X4Ycbcr(); + var pixels = AsSpan; + var ycbcrPs = rawYcbcr.AsSpan; + for (var i = 0; i < pixels.Length; i++) + { + ycbcrPs[i] = new ColorYCbCr(pixels[i]); + } + return rawYcbcr; + } + + public bool HasTransparentPixels() + { + var pixels = AsSpan; + for (var i = 0; i < pixels.Length; i++) + { + if (pixels[i].a < 255) return true; + } + return false; + } + } + + public struct RawBlock4X4RgbFloat + { + public ColorRgbFloat p00, p10, p20, p30; + public ColorRgbFloat p01, p11, p21, p31; + public ColorRgbFloat p02, p12, p22, p32; + public ColorRgbFloat p03, p13, p23, p33; + + public RawBlock4X4RgbFloat(ColorRgbFloat fillColor) + { + p00 = p01 = p02 = p03 = + p10 = p11 = p12 = p13 = + p20 = p21 = p22 = p23 = + p30 = p31 = p32 = p33 = fillColor; + } + + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public ColorRgbFloat this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public ColorRgbFloat this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + internal float CalculateError(RawBlock4X4RgbFloat other) + { + float error = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = pix1[i]; + var col2 = pix2[i]; + + var re = Math.Sign(col1.r) * MathF.Log(1 + MathF.Abs(col1.r)) - Math.Sign(col2.r) * MathF.Log(1 + MathF.Abs(col2.r)); + var ge = Math.Sign(col1.g) * MathF.Log(1 + MathF.Abs(col1.g)) - Math.Sign(col2.g) * MathF.Log(1 + MathF.Abs(col2.g)); + var be = Math.Sign(col1.b) * MathF.Log(1 + MathF.Abs(col1.b)) - Math.Sign(col2.b) * MathF.Log(1 + MathF.Abs(col2.b)); + + error += re * re; + error += ge * ge; + error += be * be; + + } + + error /= pix1.Length * 3; + error = MathF.Sqrt(error); + + return error; + } + + internal float CalculateYCbCrError(RawBlock4X4RgbFloat other) + { + float yError = 0; + float cbError = 0; + float crError = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = new ColorYCbCr(pix1[i]); + var col2 = new ColorYCbCr(pix2[i]); + + var ye = col1.y - col2.y; + var cbe = col1.cb - col2.cb; + var cre = col1.cr - col2.cr; + + yError += ye * ye; + cbError += cbe * cbe; + crError += cre * cre; + } + + var error = yError * 2 + cbError / 2 + crError / 2; + + return error; + } + + + internal RawBlock4X4Ycbcr ToRawBlockYcbcr() + { + var rawYcbcr = new RawBlock4X4Ycbcr(); + var pixels = AsSpan; + var ycbcrPs = rawYcbcr.AsSpan; + for (var i = 0; i < pixels.Length; i++) + { + ycbcrPs[i] = new ColorYCbCr(pixels[i]); + } + return rawYcbcr; + } + } + + //Used for Bc6H + internal struct RawBlock4X4RgbHalfInt + { +#pragma warning disable 0649 + public (int, int, int) p00, p10, p20, p30; + public (int, int, int) p01, p11, p21, p31; + public (int, int, int) p02, p12, p22, p32; + public (int, int, int) p03, p13, p23, p33; +#pragma warning restore 0649 + public Span<(int, int, int)> AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public (int, int, int) this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public (int, int, int) this[int index] + { + get => AsSpan[index]; + set => AsSpan[index] = value; + } + + public static RawBlock4X4RgbHalfInt FromRawFloats(RawBlock4X4RgbFloat other, bool signed) + { + var output = new RawBlock4X4RgbHalfInt(); + var span = output.AsSpan; + var floats = other.AsSpan; + for (var i = 0; i < 16; i++) + { + span[i] = Bc6EncodingHelpers.PreQuantizeRawEndpoint(floats[i], signed); + } + return output; + } + + } + + + internal struct RawBlock4X4Ycbcr + { +#pragma warning disable 0649 + public ColorYCbCr p00, p10, p20, p30; + public ColorYCbCr p01, p11, p21, p31; + public ColorYCbCr p02, p12, p22, p32; + public ColorYCbCr p03, p13, p23, p33; +#pragma warning restore 0649 + public Span AsSpan => MemoryMarshal.CreateSpan(ref p00, 16); + + public ColorYCbCr this[int x, int y] + { + get => AsSpan[x + y * 4]; + set => AsSpan[x + y * 4] = value; + } + + public float CalculateError(RawBlock4X4Rgba32 other) + { + float yError = 0; + float cbError = 0; + float crError = 0; + var pix1 = AsSpan; + var pix2 = other.AsSpan; + for (var i = 0; i < pix1.Length; i++) + { + var col1 = pix1[i]; + var col2 = new ColorYCbCr(pix2[i]); + + var ye = col1.y - col2.y; + var cbe = col1.cb - col2.cb; + var cre = col1.cr - col2.cr; + + yError += ye * ye; + cbError += cbe * cbe; + crError += cre * cre; + } + + var error = yError * 2 + cbError / 2 + crError / 2; + + return error; + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/RgbBoundingBox.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/RgbBoundingBox.cs new file mode 100644 index 0000000..c93691f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/BCnEncoder.Net/Shared/RgbBoundingBox.cs @@ -0,0 +1,276 @@ +using System.Runtime.InteropServices; + +namespace TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared +{ + /// + /// Calculate the bounding box of rgb values as described in + /// "Real-Time DXT Compression by J.M.P. van Waveren, 2006, Id Software, Inc." and + /// "Real-Time YCoCg-DXT Compression J.M.P. van Waveren, Ignacio Castaño id Software, Inc. NVIDIA Corp." + /// + internal static class RgbBoundingBox + { + + public static void Create565(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max) + { + const int colorInsetShift = 4; + const int c5655Mask = 0xF8; + const int c5656Mask = 0xFC; + + int minR = 255, + minG = 255, + minB = 255; + int maxR = 0, + maxG = 0, + maxB = 0; + + for (var i = 0; i < colors.Length; i++) + { + var c = colors[i]; + + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; + } + + var insetR = maxR - minR >> colorInsetShift; + var insetG = maxG - minG >> colorInsetShift; + var insetB = maxB - minB >> colorInsetShift; + + // Inset by 1/16th + minR = (minR << colorInsetShift) + insetR >> colorInsetShift; + minG = (minG << colorInsetShift) + insetG >> colorInsetShift; + minB = (minB << colorInsetShift) + insetB >> colorInsetShift; + + maxR = (maxR << colorInsetShift) - insetR >> colorInsetShift; + maxG = (maxG << colorInsetShift) - insetG >> colorInsetShift; + maxB = (maxB << colorInsetShift) - insetB >> colorInsetShift; + + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; + + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; + + // Optimal rounding + minR = minR & c5655Mask | minR >> 5; + minG = minG & c5656Mask | minG >> 6; + minB = minB & c5655Mask | minB >> 5; + + maxR = maxR & c5655Mask | maxR >> 5; + maxG = maxG & c5656Mask | maxG >> 6; + maxB = maxB & c5655Mask | maxB >> 5; + + min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); + max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); + } + + public static void Create565AlphaCutoff(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max, int alphaCutoff = 128) + { + const int colorInsetShift = 4; + const int c5655Mask = 0xF8; + const int c5656Mask = 0xFC; + + int minR = 255, + minG = 255, + minB = 255; + int maxR = 0, + maxG = 0, + maxB = 0; + + for (var i = 0; i < colors.Length; i++) + { + var c = colors[i]; + if (c.a < alphaCutoff) continue; + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; + } + + var insetR = maxR - minR >> colorInsetShift; + var insetG = maxG - minG >> colorInsetShift; + var insetB = maxB - minB >> colorInsetShift; + + minR = (minR << colorInsetShift) + insetR >> colorInsetShift; + minG = (minG << colorInsetShift) + insetG >> colorInsetShift; + minB = (minB << colorInsetShift) + insetB >> colorInsetShift; + + maxR = (maxR << colorInsetShift) - insetR >> colorInsetShift; + maxG = (maxG << colorInsetShift) - insetG >> colorInsetShift; + maxB = (maxB << colorInsetShift) - insetB >> colorInsetShift; + + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; + + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; + + minR = minR & c5655Mask | minR >> 5; + minG = minG & c5656Mask | minG >> 6; + minB = minB & c5655Mask | minB >> 5; + + maxR = maxR & c5655Mask | maxR >> 5; + maxG = maxG & c5656Mask | maxG >> 6; + maxB = maxB & c5655Mask | maxB >> 5; + + min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); + max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); + } + + public static void Create565A(ReadOnlySpan colors, out ColorRgb565 min, out ColorRgb565 max, out byte minAlpha, out byte maxAlpha) + { + const int colorInsetShift = 4; + const int alphaInsetShift = 5; + const int c5655Mask = 0xF8; + const int c5656Mask = 0xFC; + + int minR = 255, + minG = 255, + minB = 255, + minA = 255; + int maxR = 0, + maxG = 0, + maxB = 0, + maxA = 0; + + for (var i = 0; i < colors.Length; i++) + { + var c = colors[i]; + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + if (c.a < minA) minA = c.a; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; + if (c.a > maxA) maxA = c.a; + } + + + var insetR = maxR - minR >> colorInsetShift; + var insetG = maxG - minG >> colorInsetShift; + var insetB = maxB - minB >> colorInsetShift; + var insetA = maxA - minA >> alphaInsetShift; + + minR = (minR << colorInsetShift) + insetR >> colorInsetShift; + minG = (minG << colorInsetShift) + insetG >> colorInsetShift; + minB = (minB << colorInsetShift) + insetB >> colorInsetShift; + minA = (minA << alphaInsetShift) + insetA >> alphaInsetShift; + + maxR = (maxR << colorInsetShift) - insetR >> colorInsetShift; + maxG = (maxG << colorInsetShift) - insetG >> colorInsetShift; + maxB = (maxB << colorInsetShift) - insetB >> colorInsetShift; + maxA = (maxA << alphaInsetShift) - insetA >> alphaInsetShift; + + minR = minR >= 0 ? minR : 0; + minG = minG >= 0 ? minG : 0; + minB = minB >= 0 ? minB : 0; + minA = minA >= 0 ? minA : 0; + + maxR = maxR <= 255 ? maxR : 255; + maxG = maxG <= 255 ? maxG : 255; + maxB = maxB <= 255 ? maxB : 255; + maxA = maxA <= 255 ? maxA : 255; + + minR = minR & c5655Mask | minR >> 5; + minG = minG & c5656Mask | minG >> 6; + minB = minB & c5655Mask | minB >> 5; + + maxR = maxR & c5655Mask | maxR >> 5; + maxG = maxG & c5656Mask | maxG >> 6; + maxB = maxB & c5655Mask | maxB >> 5; + + min = new ColorRgb565((byte)minR, (byte)minG, (byte)minB); + max = new ColorRgb565((byte)maxR, (byte)maxG, (byte)maxB); + minAlpha = (byte)minA; + maxAlpha = (byte)maxA; + } + + /// + /// Hdr rgb bounding box inset by Krzysztof Narkowicz. https://github.com/knarkowicz/GPURealTimeBC6H + /// Code is public domain. + /// + private static void InsetHdrChannel(ReadOnlySpan colors, int channel, ref float blockMax, ref float blockMin) + { + var offset = 0f; + if (blockMin < 0) + { + offset = -blockMin; + blockMin += offset; + blockMax += offset; + } + + float Select(ReadOnlySpan span, int i) + { + return span[i * 3 + channel] + offset; + } + + var floats = MemoryMarshal.Cast(colors); + + var refinedBlockMin = blockMax; + var refinedBlockMax = blockMin; + + for (var i = 0; i < 16; ++i) + { + refinedBlockMin = MathF.Min(refinedBlockMin, Select(floats, i) == blockMin ? refinedBlockMin : Select(floats, i)); + refinedBlockMax = MathF.Max(refinedBlockMax, Select(floats, i) == blockMax ? refinedBlockMax : Select(floats, i)); + } + + var logRefinedBlockMax = MathF.Log(refinedBlockMax + 1.0f, 2); + var logRefinedBlockMin = MathF.Log(refinedBlockMin + 1.0f, 2); + + var logBlockMax = MathF.Log(blockMax + 1.0f, 2); + var logBlockMin = MathF.Log(blockMin + 1.0f, 2); + var logBlockMaxExt = (logBlockMax - logBlockMin) * (1.0f / 32.0f); + + logBlockMin += MathF.Min(logRefinedBlockMin - logBlockMin, logBlockMaxExt); + logBlockMax -= MathF.Min(logBlockMax - logRefinedBlockMax, logBlockMaxExt); + + blockMin = MathF.Pow(2, logBlockMin) - 1.0f - offset; + blockMax = MathF.Pow(2, logBlockMax) - 1.0f - offset; + } + + public static void CreateFloat(ReadOnlySpan colors, out ColorRgbFloat min, out ColorRgbFloat max) + { + + float minR = float.MaxValue, + minG = float.MaxValue, + minB = float.MaxValue; + float maxR = float.MinValue, + maxG = float.MinValue, + maxB = float.MinValue; + + for (var i = 0; i < colors.Length; i++) + { + var c = colors[i]; + + if (c.r < minR) minR = c.r; + if (c.g < minG) minG = c.g; + if (c.b < minB) minB = c.b; + + if (c.r > maxR) maxR = c.r; + if (c.g > maxG) maxG = c.g; + if (c.b > maxB) maxB = c.b; + } + + InsetHdrChannel(colors, 0, ref maxR, ref minR); + InsetHdrChannel(colors, 1, ref maxG, ref minG); + InsetHdrChannel(colors, 2, ref maxB, ref minB); + + min = new ColorRgbFloat(minR, minG, minB); + max = new ColorRgbFloat(maxR, maxG, maxB); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/DDS/DDSImage.cs b/src/TTGamesExplorerRebirthLib/Formats/DDS/DDSImage.cs new file mode 100644 index 0000000..def44c0 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/DDS/DDSImage.cs @@ -0,0 +1,82 @@ +using SixLabors.ImageSharp; +using System.Runtime.InteropServices; +using System.Text; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Decoder; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.Shared.ImageFiles; +using TTGamesExplorerRebirthLib.Formats.DDS.BCnEncoder.Net.ImageSharp; + +namespace TTGamesExplorerRebirthLib.Formats.DDS +{ + public class DDSImage + { + public Image[] Images; + public string Type; + public byte[] Data; + + public DDSImage(byte[] buffer) + { + Data = buffer; + + using MemoryStream stream = new(buffer); + + DdsFile ddsFile = DdsFile.Load(stream); + BcDecoder bcDecoder = new(); + + Type = Encoding.ASCII.GetString(BitConverter.GetBytes(ddsFile.header.ddsPixelFormat.dwFourCc), 0, 4); + + if (ddsFile.header.dwMipMapCount != 0) + { + Images = bcDecoder.DecodeAllMipMapsToImageRgba32(ddsFile); + } + else + { + Images = new Image[1]; + Images[0] = bcDecoder.DecodeToImageRgba32(ddsFile); + } + } + + public static uint CalculateDdsSize(MemoryStream stream, BinaryReader reader) + { + _ = reader.ReadUInt32(); // Magic: DDS. + + DdsHeader ddsHeader = MemoryMarshal.Cast(reader.ReadBytes(Marshal.SizeOf()).AsSpan())[0]; + uint ddsSize = (uint)Marshal.SizeOf() + 4; + + DdsHeaderDx10 dx10Header = new(); + + if (ddsHeader.ddsPixelFormat.IsDxt10Format) + { + ddsSize += (uint)Marshal.SizeOf(); + dx10Header = MemoryMarshal.Cast(reader.ReadBytes(Marshal.SizeOf()).AsSpan())[0]; + + stream.Seek(-Marshal.SizeOf(), SeekOrigin.Current); + } + + stream.Seek(-(Marshal.SizeOf() + 4), SeekOrigin.Current); + + uint mipMapCount = Math.Max(1, ddsHeader.dwMipMapCount); + uint faceCount = (ddsHeader.dwCaps2 & HeaderCaps2.Ddscaps2Cubemap) != 0 ? 6u : 1u; + + for (var face = 0; face < faceCount; face++) + { + DxgiFormat format = ddsHeader.ddsPixelFormat.IsDxt10Format ? dx10Header.dxgiFormat : ddsHeader.ddsPixelFormat.DxgiFormat; + uint sizeInBytes = DdsFile.GetSizeInBytes(format, ddsHeader.dwWidth, ddsHeader.dwHeight); + + for (int mip = 0; mip < mipMapCount; mip++) + { + MipMapper.CalculateMipLevelSize((int)ddsHeader.dwWidth, (int)ddsHeader.dwHeight, mip, out var mipWidth, out var mipHeight); + + if (mip > 0) // Calculate new sizeInBytes. + { + sizeInBytes = DdsFile.GetSizeInBytes(format, (uint)mipWidth, (uint)mipHeight); + } + + ddsSize += sizeInBytes; + } + } + + return ddsSize; + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/Deflatev1.cs b/src/TTGamesExplorerRebirthLib/Formats/Deflatev1.cs new file mode 100644 index 0000000..5a1be86 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/Deflatev1.cs @@ -0,0 +1,47 @@ +using ICSharpCode.SharpZipLib.Zip.Compression; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + /// + /// Give Deflate_v1.0 file data and decompress it by applying Deflate. + /// It uses RFC 1951. + /// + /// + /// Based on my own research (Ac_K). + /// + /// FIXME: AN4 files won't decompress. + /// + public static class Deflatev1 + { + private const string Magic = "Deflate_v1.0"; + + public static byte[] Decompress(byte[] fileBuffer) + { + using MemoryStream stream = new(fileBuffer); + using BinaryReader reader = new(stream); + + if (stream.ReadNullTerminatedString() != Magic) + { + stream.Seek(0x20, SeekOrigin.Begin); + + uint decompressedSize = reader.ReadUInt32(); + uint compressedSize = (uint)(stream.Length - stream.Position); + + byte[] buffer = new byte[compressedSize]; + + Array.Copy(fileBuffer, 0x24, buffer, 0, compressedSize); + + MemoryStream outputStream = new(); + + using MemoryStream compressedStream = new(buffer); + using InflaterInputStream inputStream = new(compressedStream, new Inflater(true)); + + inputStream.CopyTo(outputStream); + } + + throw new InvalidDataException($"{stream.Position:x8}"); + } + } +} diff --git a/src/TTGamesExplorerRebirthLib/Formats/FT2.cs b/src/TTGamesExplorerRebirthLib/Formats/FT2.cs new file mode 100644 index 0000000..c1333e9 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/FT2.cs @@ -0,0 +1,135 @@ +using TTGamesExplorerRebirthLib.Formats.DDS; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + public class FT2Char + { + public float X; + public float Y; + public float Width; + public float Height; + public char UnicodeChar; + public ushort FontMappingIndex; + } + + /// + /// Give ft2 file data and deserialize it. + /// + /// + /// Based on my own research (Ac_K). + /// Some fields found by dniel888. + /// + public class FT2 + { + private const string MagicNfnt = "TNFN"; + private const string MagicVtor = "ROTV"; + + public FT2Char[] Chars; + public DDSImage FontImage; + + public float MinHeight; + public float BaseLine; + public float SpaceWidth; + public uint SndId; + public uint IcGap; + + public FT2(string archiveFilePath) + { + Deserialize(File.ReadAllBytes(archiveFilePath)); + } + + public FT2(byte[] buffer) + { + Deserialize(buffer); + } + + private void Deserialize(byte[] buffer) + { + using MemoryStream stream = new(buffer); + using BinaryReader reader = new(stream); + + // Read header. + + uint headerSize = reader.ReadUInt32BigEndian(); + uint fileVersion = reader.ReadUInt32BigEndian(); // Always 1 ? + + if (reader.ReadUInt32AsString() != MagicNfnt) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint headerVersion = reader.ReadUInt32BigEndian(); + uint unknown1 = reader.ReadUInt32(); // Always 0 ? + ushort unknown2 = reader.ReadUInt16(); // Always 0 ? + uint unknownSize = reader.ReadUInt32BigEndian(); // Size ? + 0X38 give the last ROTV section + uint charsCount = reader.ReadUInt32BigEndian(); + uint unicodeTableItemCount = reader.ReadUInt32BigEndian(); // Seems to be aligned or something since the section have a lot of 0xFF + + MinHeight = reader.ReadSingleBigEndian(); + BaseLine = reader.ReadSingleBigEndian(); + SpaceWidth = reader.ReadSingleBigEndian(); + SndId = reader.ReadUInt32BigEndian(); + IcGap = reader.ReadUInt32BigEndian(); + + // Read chars mapping section. + + if (headerVersion > 2 && reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32BigEndian() != charsCount) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + Chars = new FT2Char[unicodeTableItemCount]; + + for (int i = 0; i < charsCount; i++) + { + Chars[i] = new() + { + X = reader.ReadSingleBigEndian(), + Y = reader.ReadSingleBigEndian(), + Width = reader.ReadSingleBigEndian(), + Height = MinHeight + }; + } + + // Read chars index section. + + if (headerVersion > 2 && reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32BigEndian() != unicodeTableItemCount) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + for (int i = 0; i < unicodeTableItemCount; i++) + { + if (Chars[i] == null) + { + Chars[i] = new(); + } + + Chars[i].UnicodeChar = Convert.ToChar(reader.ReadUInt16BigEndian()); + Chars[i].FontMappingIndex = reader.ReadUInt16(); + } + + // Read image section. + + if (headerVersion > 2 && reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint imageSectionSize = reader.ReadUInt32(); // Always 0. + + FontImage = new DDSImage(reader.ReadBytes((int)(stream.Length - headerSize))); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/LZ2K.cs b/src/TTGamesExplorerRebirthLib/Formats/LZ2K.cs new file mode 100644 index 0000000..8dbef39 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/LZ2K.cs @@ -0,0 +1,62 @@ +using TTGamesExplorerRebirthLib.Compression; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + /// + /// Give LZ2K file data and decompress it by applying UnLZ2K algo. + /// + /// + /// Based on QuickBMS script by Luigi Auriemma: + /// https://aluigi.altervista.org/quickbms.htm + /// + /// Based on my own research (Ac_K). + /// + public static class LZ2K + { + private const string Magic = "LZ2K"; + + public static byte[] Decompress(byte[] fileBuffer) + { + using MemoryStream inputStream = new(fileBuffer); + using MemoryStream outputStream = new(); + using BinaryReader reader = new(inputStream); + using BinaryWriter writer = new(outputStream); + + uint bytesLeft = (uint)inputStream.Length; + while (bytesLeft != 0) + { + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{inputStream.Position:x8}"); + } + + uint decompressedSize = reader.ReadUInt32(); + uint compressedSize = reader.ReadUInt32(); + + byte[] inputBuffer = new byte[compressedSize]; + + Array.Copy(fileBuffer, inputStream.Position, inputBuffer, 0, compressedSize); + + inputStream.Seek(compressedSize, SeekOrigin.Current); + + byte[] outputBuffer = new byte[decompressedSize]; + + if (compressedSize != decompressedSize) + { + UnLZ2K.Decompress(inputBuffer, outputBuffer); + } + else + { + Array.Copy(inputBuffer, outputBuffer, decompressedSize); + } + + writer.Write(outputBuffer); + + bytesLeft -= compressedSize + 0xC; + } + + return outputStream.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/NXGTextures.cs b/src/TTGamesExplorerRebirthLib/Formats/NXGTextures.cs new file mode 100644 index 0000000..389277b --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/NXGTextures.cs @@ -0,0 +1,201 @@ +using TTGamesExplorerRebirthLib.Formats.DDS; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + public class NXGFile + { + public string Path; + public byte[] Data; + } + + /// + /// Give nxg_texture file data and deserialize it. + /// + /// + /// NuTexGenHdr versioning by Jay Franco: + /// https://github.com/Smakdev/NuTCracker + /// + /// Based on my own research (Ac_K). + /// + public class NXGTextures + { + private const string Magic = ".CC4"; + private const string MagicResH = "HSER"; + private const string MagicVtor = "ROTV"; + private const string MagicTxSt = "TSXT"; + + public string ProjectName; + public string ProducedByUserName; + public string SourceFileName; + public string DateStamp; + + public List Files = []; + + public NXGTextures(string filePath) + { + Deserialize(File.ReadAllBytes(filePath)); + } + + public NXGTextures(byte[] buffer) + { + Deserialize(buffer); + } + + private void Deserialize(byte[] buffer) + { + using MemoryStream stream = new(buffer); + using BinaryReader reader = new(stream); + + // Read header. + + uint nuResourceHeaderSize = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuResourceHeaderVersion = reader.ReadUInt32BigEndian(); + uint unknown1 = reader.ReadUInt32(); + + if (reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint vtorVersion = reader.ReadUInt32BigEndian(); + uint unknown2 = reader.ReadUInt32(); + + ushort projectNameSize = reader.ReadUInt16BigEndian(); + ProjectName = stream.ReadNullTerminatedString(); + + uint unknown3 = reader.ReadUInt32(); + uint unknown4 = reader.ReadUInt32(); + + ushort producedByUserNameSize = reader.ReadUInt16BigEndian(); + ProducedByUserName = stream.ReadNullTerminatedString(); + + byte unknown5 = reader.ReadByte(); + + ushort sourceFileNameSize = reader.ReadUInt16BigEndian(); + SourceFileName = stream.ReadNullTerminatedString(); + + byte unknown6 = reader.ReadByte(); + + // Read NuTextureSetHeader. + + uint nuTextureSetHeaderSize = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicTxSt) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuTextureSetHeaderUnknown1 = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != MagicTxSt) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuTextureSetHeaderVersion = reader.ReadUInt32BigEndian(); + ushort nuTextureSetHeaderUnknown2 = reader.ReadUInt16BigEndian(); + + ushort dateStampSize = reader.ReadUInt16BigEndian(); + DateStamp = stream.ReadNullTerminatedString(); + + if (reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuTextureCount = reader.ReadUInt32BigEndian(); + + for (int i = 0; i < nuTextureCount; i++) + { + // Read NuTexGenHdr. + + byte[] nuChecksum = reader.ReadBytes(0x10); + bool isNuChecksumZeroed = !nuChecksum.Any(b => b != 0); + + string path = ""; + + if (nuTextureSetHeaderVersion == 1) + { + uint pathSize = reader.ReadUInt32BigEndian(); + path = stream.ReadNullTerminatedString(); + uint nuTexGenHdrUnknown1 = reader.ReadUInt32BigEndian(); + } + + if (nuTextureSetHeaderVersion == 12) + { + uint nuAlignedBuffer = reader.ReadUInt32(); + byte pathSize = reader.ReadByte(); + path = stream.ReadNullTerminatedString(); + byte nuResourceId = reader.ReadByte(); + } + + // FIXME: Some nxg_textures failed here. + if (nuTextureSetHeaderVersion == 14) + { + if (!isNuChecksumZeroed) + { + uint nuAlignedBuffer = reader.ReadUInt32(); + byte pathSize = reader.ReadByte(); + path = stream.ReadNullTerminatedString(); + byte nuResourceId = reader.ReadByte(); + uint nuTexGenHdrUnknown3 = reader.ReadUInt32(); + uint nuTexGenHdrUnknown4 = reader.ReadUInt32(); + } + else + { + ushort pathSize = reader.ReadUInt16BigEndian(); + path = stream.ReadNullTerminatedString(); + uint nuTexGenHdrUnknown1 = reader.ReadUInt32(); + uint nuTexGenHdrUnknown2 = reader.ReadUInt32(); + uint nuTexGenHdrUnknown3 = reader.ReadUInt32(); + } + } + + if (!isNuChecksumZeroed) + { + Files.Add(new NXGFile() + { + Path = path, + // TODO: Add more informations. + }); + } + } + + for (int i = 0; i < Files.Count; i++) + { + uint ddsSize = DDSImage.CalculateDdsSize(stream, reader); + + Files[i].Data = reader.ReadBytes((int)ddsSize); + + if (stream.Position == stream.Length) + { + Files = Files.Take(i + 1).Skip(Files.Count - (i + 1)).ToList(); + + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/PAK.cs b/src/TTGamesExplorerRebirthLib/Formats/PAK.cs new file mode 100644 index 0000000..5b19cd2 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/PAK.cs @@ -0,0 +1,98 @@ +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + public class PAKFile + { + public string Name; + public uint Offset; + public uint Size; + public byte[] Data; + } + + /// + /// Give pak file data and deserialize it. + /// + /// + /// Based on my own research (Ac_K). + /// + public class PAK + { + private const uint Magic = 0x1234567A; + + public List Files = []; + + public PAK(string archiveFilePath) + { + Deserialize(File.ReadAllBytes(archiveFilePath)); + } + + public PAK(byte[] buffer) + { + Deserialize(buffer); + } + + private void Deserialize(byte[] buffer) + { + using MemoryStream stream = new(buffer); + using BinaryReader reader = new(stream); + + // Read header. + + if (reader.ReadUInt32() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint fileCount = reader.ReadUInt32(); + uint archiveSize = reader.ReadUInt32(); + uint checksum = reader.ReadUInt32(); + uint unknown1 = reader.ReadUInt32(); // Always 0. + uint unknown2 = reader.ReadUInt32(); // TODO: Known values: 0x00402F4C. + + // NOTE: Checksum is computed over the whole file with the field "checksum" zeroed. + // It uses TTGamesChecksum.PAK(buffer); + + // Iterate over each file info. + + for (int i = 0; i < fileCount; i++) + { + // Read file info. + + uint nameOffset = reader.ReadUInt32(); + uint fileOffset = reader.ReadUInt32(); + uint fileSize = reader.ReadUInt32(); + uint flag = reader.ReadUInt32(); // TODO: Filetype ? (0 > Cubemap Texture, 1 > Text, 4 > Texture, 10 > Animation + uint unknownFile1 = reader.ReadUInt32(); // Always 0. + uint unknownHash1 = reader.ReadUInt32(); // TODO: Determine what and how it's hashed here. (File name? File data?) + uint unknownHash2 = reader.ReadUInt32(); // TODO: Determine what and how it's hashed here. (File name? File data?). + + // Read file name. + + long oldPosition = stream.Position; + + stream.Seek(nameOffset, SeekOrigin.Begin); + + string fileName = stream.ReadNullTerminatedString().ToLowerInvariant(); + + stream.Seek(oldPosition, SeekOrigin.Begin); + + Files.Add(new() + { + Name = fileName, + Offset = fileOffset, + Size = fileSize, + }); + } + + for (int i = 0; i < fileCount; i++) + { + // Read file data. + + stream.Seek(Files[i].Offset, SeekOrigin.Begin); + + Files[i].Data = reader.ReadBytes((int)Files[i].Size); + } + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/PCShaders.cs b/src/TTGamesExplorerRebirthLib/Formats/PCShaders.cs new file mode 100644 index 0000000..c4fae91 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/PCShaders.cs @@ -0,0 +1,158 @@ +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + public class PCShadersFile + { + public PCShadersType Type; + public byte[] Data; + } + + public enum PCShadersType + { + DXBC, + CTAB, + } + + /// + /// Give pc_shaders file data and deserialize it. + /// + /// + /// Based on my own research (Ac_K). + /// + public class PCShaders + { + private const string Magic = ".CC4"; + private const string MagicResH = "HSER"; + private const string MagicVtor = "ROTV"; + private const string MagicBcsh = "HSCB"; + + public string ProjectName; + + public List Shaders = []; + + public PCShaders(string filePath) + { + Deserialize(File.ReadAllBytes(filePath)); + } + + public PCShaders(byte[] buffer) + { + Deserialize(buffer); + } + + private void Deserialize(byte[] buffer) + { + using MemoryStream stream = new(buffer); + using BinaryReader reader = new(stream); + + // Read header. + + uint nuResourceHeaderSize = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint unknown1 = reader.ReadUInt32BigEndian(); // Always 8 ? + uint unknown2 = reader.ReadUInt32BigEndian(); // Always 0 ? + + if (reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint unknown3 = reader.ReadUInt32BigEndian(); // Always 0 ? + uint unknown4 = reader.ReadUInt32BigEndian(); // Always 14 ? + ushort unknown5 = reader.ReadUInt16BigEndian(); // Always 1 ? + ushort unknown6 = reader.ReadUInt16BigEndian(); // Always 0 ? + uint unknown7 = reader.ReadUInt32BigEndian(); // Always 0 ? + uint unknown8 = reader.ReadUInt32BigEndian(); // Always 0 ? + byte unknown9 = reader.ReadByte(); // Always 1 ? + byte unknown10 = reader.ReadByte(); // Always 0 ? + byte unknown11 = reader.ReadByte(); // Always 0xFF ? + + ushort projectNameSize = reader.ReadUInt16BigEndian(); + ProjectName = stream.ReadNullTerminatedString(); + + byte unknown12 = reader.ReadByte(); // Always 2 ? + + // Read subheader. + + uint dataSize = reader.ReadUInt32BigEndian(); // dataSize + nuResourceHeaderSize + 8 (2 int for sizes) = Total filesize. + + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint unknown13 = reader.ReadUInt32BigEndian(); // Always 2 ? + uint unknown14 = reader.ReadUInt32BigEndian(); // Always 0x808 ? + + if (reader.ReadUInt32AsString() != MagicBcsh) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint unknown15 = reader.ReadUInt32BigEndian(); // Always 12 ? + uint unknown16 = reader.ReadUInt32BigEndian(); // It's the total of shader files / 2 (Why?) + + // Read shader files. + + int i = 0; + while (stream.Position < stream.Length - 4) + { + stream.Seek((i % 2 == 0) ? 0x1A : 0xA, SeekOrigin.Current); + + ushort shaderSize = reader.ReadUInt16BigEndian(); + PCShadersType shaderType = PCShadersType.DXBC; + + if (reader.ReadUInt16().ToConvertedString() != "DX") + { + stream.Seek(-2, SeekOrigin.Current); + reader.ReadByte(); + + if (reader.ReadByte() != 3) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + shaderType = PCShadersType.CTAB; + } + + stream.Seek(-2, SeekOrigin.Current); + + Shaders.Add(new() + { + Type = shaderType, + Data = reader.ReadBytes(shaderSize), + }); + + i++; + } + + uint unknown17 = reader.ReadUInt32BigEndian(); // Each file finish with a value here. + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/TSH.cs b/src/TTGamesExplorerRebirthLib/Formats/TSH.cs new file mode 100644 index 0000000..a7ee9b8 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/TSH.cs @@ -0,0 +1,173 @@ +using System.Text; +using TTGamesExplorerRebirthLib.Formats.DDS; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + public class TSHEntry + { + public float MinU; + public float MinV; + public float MaxU; + public float MaxV; + + public uint MinX; + public uint MinY; + public uint Width; + public uint Height; + + public string Magic; + + public int TrimTop; + public int TrimBottom; + public int TrimLeft; + public int TrimRight; + + public uint NameHash; // FNV132 hash in uppercase (_content.txt file for the list) + } + + /// + /// Give tsh file (NuTextureSheet) data and deserialize it. + /// + /// + /// Based on my own research (Ac_K). + /// + /// Some field names found by Jay Franco. + /// + public class TSH + { + private const string Magic = ".CC4"; + private const string MagicResH = "HSER"; + private const string MagicVtor = "ROTV"; + private const string MagicTxSh = "HSXT"; + + public string ProjectName; + public string ProducedByUserName; + public string SourceFileName; + + public DDSImage Image; + + public List Entries = []; + + public TSH(string archiveFilePath) + { + Deserialize(File.ReadAllBytes(archiveFilePath)); + } + + public TSH(byte[] buffer) + { + Deserialize(buffer); + } + + private void Deserialize(byte[] buffer) + { + using MemoryStream stream = new(buffer); + using BinaryReader reader = new(stream); + + // Read header. + + uint nuResourceHeaderSize = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicResH) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuResourceHeaderVersion = reader.ReadUInt32BigEndian(); + uint unknown1 = reader.ReadUInt32(); + + if (reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint vtorVersion = reader.ReadUInt32BigEndian(); + uint unknown2 = reader.ReadUInt32(); + + ushort projectNameSize = reader.ReadUInt16BigEndian(); + ProjectName = stream.ReadNullTerminatedString(); + + uint unknown3 = reader.ReadUInt32(); + uint unknown4 = reader.ReadUInt32(); + + ushort producedByUserNameSize = reader.ReadUInt16BigEndian(); + ProducedByUserName = stream.ReadNullTerminatedString(); + + byte unknown5 = reader.ReadByte(); + + ushort sourceFileNameSize = reader.ReadUInt16BigEndian(); + SourceFileName = stream.ReadNullTerminatedString(); + + byte unknown6 = reader.ReadByte(); + + // Read NuTextureSheetHeader. + + uint nuTextureSetHeaderSize = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicTxSh) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + if (reader.ReadUInt32AsString() != MagicTxSh) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuTextureSetHeaderVersion = reader.ReadUInt32BigEndian(); + + if (reader.ReadUInt32AsString() != MagicVtor) + { + throw new InvalidDataException($"{stream.Position:x8}"); + } + + uint nuTextureInfoCount = reader.ReadUInt32BigEndian(); + + // Read entries. + + for (int i = 0; i < nuTextureInfoCount; i++) + { + TSHEntry entry = new() + { + MinU = reader.ReadSingleBigEndian(), + MinV = reader.ReadSingleBigEndian(), + MaxU = reader.ReadSingleBigEndian(), + MaxV = reader.ReadSingleBigEndian(), + + MinX = reader.ReadUInt32BigEndian(), + MinY = reader.ReadUInt32BigEndian(), + Width = reader.ReadUInt32BigEndian(), + Height = reader.ReadUInt32BigEndian(), + + Magic = Encoding.ASCII.GetString(reader.ReadBytes(4)), + + TrimTop = reader.ReadInt32BigEndian(), + TrimBottom = reader.ReadInt32BigEndian(), + TrimLeft = reader.ReadInt32BigEndian(), + TrimRight = reader.ReadInt32BigEndian(), + + NameHash = reader.ReadUInt32() + }; + + Entries.Add(entry); + } + + Image = new DDSImage(reader.ReadBytes((int)(stream.Length - (int)stream.Position))); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Formats/ZIPX.cs b/src/TTGamesExplorerRebirthLib/Formats/ZIPX.cs new file mode 100644 index 0000000..8cf8c9a --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Formats/ZIPX.cs @@ -0,0 +1,51 @@ +using TTGamesExplorerRebirthLib.Encryption; +using TTGamesExplorerRebirthLib.Helper; + +namespace TTGamesExplorerRebirthLib.Formats +{ + /// + /// Give ZIPX file data and decrypt it by applying RC4 cryptography. + /// + /// + /// Based on QuickBMS script by Luigi Auriemma: + /// https://aluigi.altervista.org/quickbms.htm + /// + public static class ZIPX + { + private const string Magic = "ZIPX"; + + public static byte[] Decrypt(byte[] fileBuffer) + { + using MemoryStream inputStream = new(fileBuffer); + using MemoryStream outputStream = new(); + using BinaryReader reader = new(inputStream); + using BinaryWriter writer = new(outputStream); + + uint bytesLeft = (uint)inputStream.Length; + while (bytesLeft != 0) + { + if (reader.ReadUInt32AsString() != Magic) + { + throw new InvalidDataException($"{inputStream.Position:x8}"); + } + + uint compressedSize = reader.ReadUInt32(); + uint decompressedSize = reader.ReadUInt32(); + + byte[] inputBuffer = new byte[decompressedSize]; + + Array.Copy(fileBuffer, inputStream.Position, inputBuffer, 0, decompressedSize); + + inputStream.Seek(decompressedSize, SeekOrigin.Current); + + byte[] outputBuffer = RC4.Crypt(inputBuffer, BitConverter.GetBytes(decompressedSize)); + + writer.Write(outputBuffer); + + bytesLeft -= decompressedSize + 0xC; + } + + return outputStream.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Hash/Fnv.cs b/src/TTGamesExplorerRebirthLib/Hash/Fnv.cs new file mode 100644 index 0000000..29b2bb6 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Hash/Fnv.cs @@ -0,0 +1,172 @@ +using System; +using System.Text; + +namespace TTGamesExplorerRebirthLib.Hash +{ + public static class Fnv + { + public static uint Fnv_32(string text, bool alternate = false) + { + return Fnv_32(Encoding.ASCII.GetBytes(text), alternate); + } + + public static uint Fnv_32(byte[] buffer, bool alternate = false) + { + uint prime = 0x1000193; + uint hash = 0; + + foreach (byte b in buffer) + { + hash = alternate ? (hash ^ b) * prime : (hash * prime) ^ b; + } + + return hash; + } + + public static uint Fnv1a_32_TTGames(string text) + { + uint prime = 0x199933; + uint hash = 0x811C9DC5; + + foreach (byte b in Encoding.ASCII.GetBytes(text.ToUpperInvariant())) + { + hash = (hash ^ b) * prime; + } + + return hash; + } + + public static uint Fnv1_32(string text, bool alternate = false) + { + return Fnv1_32(Encoding.ASCII.GetBytes(text), alternate); + } + + public static uint Fnv1_32(byte[] buffer, bool alternate = false) + { + uint prime = 0x1000193; + uint hash = 0x811C9DC5; + + foreach (byte b in buffer) + { + hash = alternate ? (hash ^ b) * prime : (hash * prime) ^ b; + } + + return hash; + } + + public static ulong Fnv1_64(byte[] buffer, bool alternate = false) + { + ulong prime = 0x100000001B3; + ulong hash = 0xCBF29CE484222325; + + foreach (byte b in buffer) + { + hash = alternate ? (hash ^ b) * prime : (hash * prime) ^ b; + } + + return hash; + } + + public static byte[] Fnv1_128(byte[] buffer, bool alternate = false) + { + uint primeB = 0x01000000; + uint primeD = 0x0000013B; + uint primeBD = 0xFFFEC5; + + ulong baseH = 0x6c62272e07bb0142; + ulong baseL = 0x62b821756295c58d; + + ulong a = baseH >> 32, b = (uint)baseH, c = baseL >> 32, d = (uint)baseL; + + ulong f = 0, fLm = 0; + int i = 0; + unchecked + { + for (; i < buffer.Length; ++i) + { + if (!alternate) + { + d ^= buffer[i]; + } + + // Below is an optimized implementation (limited) of the LX4Cnh algorithm specially for Fnv1a128 + // (c) Denis Kuzmin github/3F + + f = b * primeB; + + ulong v = (uint)f; + + f = (f >> 32) + v; + + if (a > b) + { + f += (uint)((a - b) * primeB); + } + else if (a < b) + { + f -= (uint)((b - a) * primeB); + } + + ulong fHigh = (f << 32) + (uint)v; + ulong r2 = d * primeD; + + v = (r2 >> 32) + (r2 & 0xFFF_FFFF_FFFF_FFFF); + + f = (r2 & 0xF000_0000_0000_0000) >> 32; + + if (c > d) + { + fLm = v; + v += (c - d) * primeD; + if (fLm > v) f += 0x100000000; + } + else if (c < d) + { + fLm = v; + v -= (d - c) * primeD; + if (fLm < v) f -= 0x100000000; + } + + fLm = (((ulong)(uint)v) << 32) + (uint)r2; + + f = fHigh + fLm + f + (v >> 32); + + fHigh = (a << 32) + b; // fa + v = (c << 32) + d; // fb + + if (fHigh < v) + { + f += (v - fHigh) * primeBD; + } + else if (fHigh > v) + { + f -= (fHigh - v) * primeBD; + } + + a = f >> 32; + b = (uint)f; + c = fLm >> 32; + d = (uint)fLm; + + if (alternate) + { + d ^= buffer[i]; + } + } + } + + ulong low = 0; + + if (i < 1) + { + low = baseL; + + f = baseH; + } + + low = fLm; + + return [.. BitConverter.GetBytes(f).Reverse(), .. BitConverter.GetBytes(low).Reverse()]; + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Hash/TTGamesChecksum.cs b/src/TTGamesExplorerRebirthLib/Hash/TTGamesChecksum.cs new file mode 100644 index 0000000..569aff6 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Hash/TTGamesChecksum.cs @@ -0,0 +1,17 @@ +namespace TTGamesExplorerRebirthLib.Hash +{ + public static class TTGamesChecksum + { + public static int PAK(byte[] buffer) + { + int checksum = 0x12345678; + + foreach (byte b in buffer) + { + checksum += b; + } + + return checksum; + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Helper/BinaryReaderEx.cs b/src/TTGamesExplorerRebirthLib/Helper/BinaryReaderEx.cs new file mode 100644 index 0000000..7fa0f0a --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Helper/BinaryReaderEx.cs @@ -0,0 +1,58 @@ +using System.Buffers.Binary; +using System.Text; + +namespace TTGamesExplorerRebirthLib.Helper +{ + public static class BinaryReaderEx + { + public static string ReadUInt64AsString(this BinaryReader reader) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(reader.ReadUInt64()), 0, 8); + } + + public static string ReadUInt32AsString(this BinaryReader reader) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(reader.ReadUInt32()), 0, 4); + } + + public static double ReadDoubleBigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadDoubleBigEndian(reader.ReadBytes(8)); + } + + public static short ReadInt16BigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2)); + } + + public static int ReadInt32BigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4)); + } + + public static long ReadInt64BigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadInt64BigEndian(reader.ReadBytes(8)); + } + + public static float ReadSingleBigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadSingleBigEndian(reader.ReadBytes(4)); + } + + public static ushort ReadUInt16BigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadUInt16BigEndian(reader.ReadBytes(2)); + } + + public static uint ReadUInt32BigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(4)); + } + + public static ulong ReadUInt64BigEndian(this BinaryReader reader) + { + return BinaryPrimitives.ReadUInt64BigEndian(reader.ReadBytes(8)); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Helper/BinaryWriterEx.cs b/src/TTGamesExplorerRebirthLib/Helper/BinaryWriterEx.cs new file mode 100644 index 0000000..0533e42 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Helper/BinaryWriterEx.cs @@ -0,0 +1,12 @@ +using System.Text; + +namespace TTGamesExplorerRebirthLib.Helper +{ + public static class BinaryWriterEx + { + public static void WriteStringWithoutPrefixedSize(this BinaryWriter writer, string value) + { + writer.Write(Encoding.ASCII.GetBytes(value)); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Helper/Helper.cs b/src/TTGamesExplorerRebirthLib/Helper/Helper.cs new file mode 100644 index 0000000..9b70ec5 --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Helper/Helper.cs @@ -0,0 +1,37 @@ +using System.Text; + +namespace TTGamesExplorerRebirthLib.Helper +{ + public static class Helper + { + public static string ToConvertedString(this long value) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(value), 0, 8); + } + + public static string ToConvertedString(this ulong value) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(value), 0, 8); + } + + public static string ToConvertedString(this int value) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(value), 0, 4); + } + + public static string ToConvertedString(this uint value) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(value), 0, 4); + } + + public static string ToConvertedString(this short value) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(value), 0, 2); + } + + public static string ToConvertedString(this ushort value) + { + return Encoding.ASCII.GetString(BitConverter.GetBytes(value), 0, 2); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/Helper/StreamEx.cs b/src/TTGamesExplorerRebirthLib/Helper/StreamEx.cs new file mode 100644 index 0000000..8f67aec --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/Helper/StreamEx.cs @@ -0,0 +1,21 @@ +using System.Text; + +namespace TTGamesExplorerRebirthLib.Helper +{ + public static class StreamEx + { + public static string ReadNullTerminatedString(this Stream Stream) + { + List strBytes = []; + + int b; + + while ((b = Stream.ReadByte()) != 0x00) + { + strBytes.Add((byte)b); + } + + return Encoding.ASCII.GetString(strBytes.ToArray()); + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthLib/TTGamesExplorerRebirthLib.csproj b/src/TTGamesExplorerRebirthLib/TTGamesExplorerRebirthLib.csproj new file mode 100644 index 0000000..b650a3f --- /dev/null +++ b/src/TTGamesExplorerRebirthLib/TTGamesExplorerRebirthLib.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + disable + true + + + + + + + + + diff --git a/src/TTGamesExplorerRebirthUI/AppSettings.cs b/src/TTGamesExplorerRebirthUI/AppSettings.cs new file mode 100644 index 0000000..ab542a7 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/AppSettings.cs @@ -0,0 +1,39 @@ +using System.Text.Json; + +namespace TTGamesExplorerRebirthUI +{ + public class AppSettings + { + private const string SettingsFilePath = "settings.json"; + + public uint Version { get; set; } + public string GameFolderPath { get; set; } + + private static AppSettings _instance; + + public static AppSettings Instance + { + get + { + _instance ??= new AppSettings(); + + _instance.Version = 1; + + return _instance; + } + } + + public void Save() + { + File.WriteAllText(SettingsFilePath, JsonSerializer.Serialize(_instance)); + } + + public void Load() + { + if (File.Exists(SettingsFilePath)) + { + _instance = JsonSerializer.Deserialize(File.ReadAllText(SettingsFilePath)); + } + } + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/AutocompleteItem.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/AutocompleteItem.cs new file mode 100644 index 0000000..59feec4 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/AutocompleteItem.cs @@ -0,0 +1,266 @@ +using System; +using System.Drawing; +using System.Drawing.Printing; + +namespace FastColoredTextBoxNS +{ + /// + /// Item of autocomplete menu + /// + public class AutocompleteItem + { + public string Text; + public int ImageIndex = -1; + public object Tag; + string toolTipTitle; + string toolTipText; + string menuText; + public AutocompleteMenu Parent { get; internal set; } + + + public AutocompleteItem() + { + } + + public AutocompleteItem(string text) + { + Text = text; + } + + public AutocompleteItem(string text, int imageIndex) + : this(text) + { + this.ImageIndex = imageIndex; + } + + public AutocompleteItem(string text, int imageIndex, string menuText) + : this(text, imageIndex) + { + this.menuText = menuText; + } + + public AutocompleteItem(string text, int imageIndex, string menuText, string toolTipTitle, string toolTipText) + : this(text, imageIndex, menuText) + { + this.toolTipTitle = toolTipTitle; + this.toolTipText = toolTipText; + } + + /// + /// Returns text for inserting into Textbox + /// + public virtual string GetTextForReplace() + { + return Text; + } + + /// + /// Compares fragment text with this item + /// + public virtual CompareResult Compare(string fragmentText) + { + if (Text.StartsWith(fragmentText, StringComparison.InvariantCultureIgnoreCase) && + Text != fragmentText) + return CompareResult.VisibleAndSelected; + + return CompareResult.Hidden; + } + + /// + /// Returns text for display into popup menu + /// + public override string ToString() + { + return menuText ?? Text; + } + + /// + /// This method is called after item inserted into text + /// + public virtual void OnSelected(AutocompleteMenu popupMenu, SelectedEventArgs e) + { + ; + } + + /// + /// Title for tooltip. + /// + /// Return null for disable tooltip for this item + public virtual string ToolTipTitle + { + get { return toolTipTitle; } + set { toolTipTitle = value; } + } + + /// + /// Tooltip text. + /// + /// For display tooltip text, ToolTipTitle must be not null + public virtual string ToolTipText + { + get{ return toolTipText; } + set { toolTipText = value; } + } + + /// + /// Menu text. This text is displayed in the drop-down menu. + /// + public virtual string MenuText + { + get { return menuText; } + set { menuText = value; } + } + + /// + /// Fore color of text of item + /// + public virtual Color ForeColor + { + get { return Color.Transparent; } + set { throw new NotImplementedException("Override this property to change color"); } + } + + /// + /// Back color of item + /// + public virtual Color BackColor + { + get { return Color.Transparent; } + set { throw new NotImplementedException("Override this property to change color"); } + } + } + + public enum CompareResult + { + /// + /// Item do not appears + /// + Hidden, + /// + /// Item appears + /// + Visible, + /// + /// Item appears and will selected + /// + VisibleAndSelected + } + + /// + /// Autocomplete item for code snippets + /// + /// Snippet can contain special char ^ for caret position. + public class SnippetAutocompleteItem : AutocompleteItem + { + public SnippetAutocompleteItem(string snippet) + { + Text = snippet.Replace("\r", ""); + ToolTipTitle = "Code snippet:"; + ToolTipText = Text; + } + + public override string ToString() + { + return MenuText ?? Text.Replace("\n", " ").Replace("^", ""); + } + + public override string GetTextForReplace() + { + return Text; + } + + public override void OnSelected(AutocompleteMenu popupMenu, SelectedEventArgs e) + { + e.Tb.BeginUpdate(); + e.Tb.Selection.BeginUpdate(); + //remember places + var p1 = popupMenu.Fragment.Start; + var p2 = e.Tb.Selection.Start; + //do auto indent + if (e.Tb.AutoIndent) + { + for (int iLine = p1.iLine + 1; iLine <= p2.iLine; iLine++) + { + e.Tb.Selection.Start = new Place(0, iLine); + e.Tb.DoAutoIndent(iLine); + } + } + e.Tb.Selection.Start = p1; + //move caret position right and find char ^ + while (e.Tb.Selection.CharBeforeStart != '^') + if (!e.Tb.Selection.GoRightThroughFolded()) + break; + //remove char ^ + e.Tb.Selection.GoLeft(true); + e.Tb.InsertText(""); + // + e.Tb.Selection.EndUpdate(); + e.Tb.EndUpdate(); + } + + /// + /// Compares fragment text with this item + /// + public override CompareResult Compare(string fragmentText) + { + if (Text.StartsWith(fragmentText, StringComparison.InvariantCultureIgnoreCase) && + Text != fragmentText) + return CompareResult.Visible; + + return CompareResult.Hidden; + } + } + + /// + /// This autocomplete item appears after dot + /// + public class MethodAutocompleteItem : AutocompleteItem + { + string firstPart; + string lowercaseText; + + public MethodAutocompleteItem(string text) + : base(text) + { + lowercaseText = Text.ToLower(); + } + + public override CompareResult Compare(string fragmentText) + { + int i = fragmentText.LastIndexOf('.'); + if (i < 0) + return CompareResult.Hidden; + string lastPart = fragmentText.Substring(i + 1); + firstPart = fragmentText.Substring(0, i); + + if(lastPart=="") return CompareResult.Visible; + if(Text.StartsWith(lastPart, StringComparison.InvariantCultureIgnoreCase)) + return CompareResult.VisibleAndSelected; + if(lowercaseText.Contains(lastPart.ToLower())) + return CompareResult.Visible; + + return CompareResult.Hidden; + } + + public override string GetTextForReplace() + { + return firstPart + "." + Text; + } + } + + /// + /// This Item does not check correspondence to current text fragment. + /// SuggestItem is intended for dynamic menus. + /// + public class SuggestItem : AutocompleteItem + { + public SuggestItem(string text, int imageIndex):base(text, imageIndex) + { + } + + public override CompareResult Compare(string fragmentText) + { + return CompareResult.Visible; + } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/AutocompleteMenu.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/AutocompleteMenu.cs new file mode 100644 index 0000000..829783a --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/AutocompleteMenu.cs @@ -0,0 +1,792 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using System.Drawing; +using System.ComponentModel; +using System.Drawing.Drawing2D; +using System.Text.RegularExpressions; + +namespace FastColoredTextBoxNS +{ + /// + /// Popup menu for autocomplete + /// + [Browsable(false)] + public class AutocompleteMenu : ToolStripDropDown, IDisposable + { + AutocompleteListView listView; + public ToolStripControlHost host; + public Range Fragment { get; internal set; } + + /// + /// Regex pattern for serach fragment around caret + /// + public string SearchPattern { get; set; } + /// + /// Minimum fragment length for popup + /// + public int MinFragmentLength { get; set; } + /// + /// User selects item + /// + public event EventHandler Selecting; + /// + /// It fires after item inserting + /// + public event EventHandler Selected; + /// + /// Occurs when popup menu is opening + /// + public new event EventHandler Opening; + /// + /// Allow TAB for select menu item + /// + public bool AllowTabKey { get { return listView.AllowTabKey; } set { listView.AllowTabKey = value; } } + /// + /// Interval of menu appear (ms) + /// + public int AppearInterval { get { return listView.AppearInterval; } set { listView.AppearInterval = value; } } + /// + /// Sets the max tooltip window size + /// + public Size MaxTooltipSize { get { return listView.MaxToolTipSize; } set { listView.MaxToolTipSize = value; } } + /// + /// Tooltip will perm show and duration will be ignored + /// + public bool AlwaysShowTooltip { get { return listView.AlwaysShowTooltip; } set { listView.AlwaysShowTooltip = value; } } + + /// + /// Back color of selected item + /// + [DefaultValue(typeof(Color), "Orange")] + public Color SelectedColor + { + get { return listView.SelectedColor; } + set { listView.SelectedColor = value; } + } + + /// + /// Border color of hovered item + /// + [DefaultValue(typeof(Color), "Red")] + public Color HoveredColor + { + get { return listView.HoveredColor; } + set { listView.HoveredColor = value; } + } + + public AutocompleteMenu(FastColoredTextBox tb) + { + // create a new popup and add the list view to it + AutoClose = false; + AutoSize = false; + Margin = Padding.Empty; + Padding = Padding.Empty; + BackColor = Color.White; + listView = new AutocompleteListView(tb); + host = new ToolStripControlHost(listView); + host.Margin = new Padding(2, 2, 2, 2); + host.Padding = Padding.Empty; + host.AutoSize = false; + host.AutoToolTip = false; + CalcSize(); + base.Items.Add(host); + listView.Parent = this; + SearchPattern = @"[\w\.]"; + MinFragmentLength = 2; + + } + + public new Font Font + { + get { return listView.Font; } + set { listView.Font = value; } + } + + new internal void OnOpening(CancelEventArgs args) + { + if (Opening != null) + Opening(this, args); + } + + public new void Close() + { + listView.toolTip.Hide(listView); + base.Close(); + } + + internal void CalcSize() + { + host.Size = listView.Size; + Size = new System.Drawing.Size(listView.Size.Width + 4, listView.Size.Height + 4); + } + + public virtual void OnSelecting() + { + listView.OnSelecting(); + } + + public void SelectNext(int shift) + { + listView.SelectNext(shift); + } + + internal void OnSelecting(SelectingEventArgs args) + { + if (Selecting != null) + Selecting(this, args); + } + + public void OnSelected(SelectedEventArgs args) + { + if (Selected != null) + Selected(this, args); + } + + public new AutocompleteListView Items + { + get { return listView; } + } + + /// + /// Shows popup menu immediately + /// + /// If True - MinFragmentLength will be ignored + public void Show(bool forced) + { + Items.DoAutocomplete(forced); + } + + /// + /// Minimal size of menu + /// + public new Size MinimumSize + { + get { return Items.MinimumSize; } + set { Items.MinimumSize = value; } + } + + /// + /// Image list of menu + /// + public new ImageList ImageList + { + get { return Items.ImageList; } + set { Items.ImageList = value; } + } + + /// + /// Tooltip duration (ms) + /// + public int ToolTipDuration + { + get { return Items.ToolTipDuration; } + set { Items.ToolTipDuration = value; } + } + + /// + /// Tooltip + /// + public ToolTip ToolTip + { + get { return Items.toolTip; } + set { Items.toolTip = value; } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (listView != null && !listView.IsDisposed) + listView.Dispose(); + } + } + + [System.ComponentModel.ToolboxItem(false)] + public class AutocompleteListView : UserControl, IDisposable + { + public event EventHandler FocussedItemIndexChanged; + + internal List visibleItems; + IEnumerable sourceItems = new List(); + int focussedItemIndex = 0; + int hoveredItemIndex = -1; + + private int ItemHeight + { + get { return Font.Height + 2; } + } + + AutocompleteMenu Menu { get { return Parent as AutocompleteMenu; } } + int oldItemCount = 0; + FastColoredTextBox tb; + internal ToolTip toolTip = new ToolTip(); + System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer(); + + internal bool AllowTabKey { get; set; } + public ImageList ImageList { get; set; } + internal int AppearInterval { get { return timer.Interval; } set { timer.Interval = value; } } + internal int ToolTipDuration { get; set; } + internal Size MaxToolTipSize { get; set; } + internal bool AlwaysShowTooltip + { + get { return toolTip.ShowAlways; } + set { toolTip.ShowAlways = value; } + } + + public Color SelectedColor { get; set; } + public Color HoveredColor { get; set; } + public int FocussedItemIndex + { + get { return focussedItemIndex; } + set + { + if (focussedItemIndex != value) + { + focussedItemIndex = value; + if (FocussedItemIndexChanged != null) + FocussedItemIndexChanged(this, EventArgs.Empty); + } + } + } + + public AutocompleteItem FocussedItem + { + get + { + if (FocussedItemIndex >= 0 && focussedItemIndex < visibleItems.Count) + return visibleItems[focussedItemIndex]; + return null; + } + set + { + FocussedItemIndex = visibleItems.IndexOf(value); + } + } + + internal AutocompleteListView(FastColoredTextBox tb) + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); + base.Font = new Font(FontFamily.GenericSansSerif, 9); + visibleItems = new List(); + VerticalScroll.SmallChange = ItemHeight; + MaximumSize = new Size(Size.Width, 180); + toolTip.ShowAlways = false; + AppearInterval = 500; + timer.Tick += new EventHandler(timer_Tick); + SelectedColor = Color.Orange; + HoveredColor = Color.Red; + ToolTipDuration = 3000; + toolTip.Popup += ToolTip_Popup; + + this.tb = tb; + + tb.KeyDown += new KeyEventHandler(tb_KeyDown); + tb.SelectionChanged += new EventHandler(tb_SelectionChanged); + tb.KeyPressed += new KeyPressEventHandler(tb_KeyPressed); + + Form form = tb.FindForm(); + if (form != null) + { + form.LocationChanged += delegate { SafetyClose(); }; + form.ResizeBegin += delegate { SafetyClose(); }; + form.FormClosing += delegate { SafetyClose(); }; + form.LostFocus += delegate { SafetyClose(); }; + } + + tb.LostFocus += (o, e) => + { + if (Menu != null && !Menu.IsDisposed) + if (!Menu.Focused) + SafetyClose(); + }; + + tb.Scroll += delegate { SafetyClose(); }; + + this.VisibleChanged += (o, e) => + { + if (this.Visible) + DoSelectedVisible(); + }; + } + + private void ToolTip_Popup(object sender, PopupEventArgs e) + { + if (MaxToolTipSize.Height > 0 && MaxToolTipSize.Width > 0) + e.ToolTipSize = MaxToolTipSize; + } + + protected override void Dispose(bool disposing) + { + if (toolTip != null) + { + toolTip.Popup -= ToolTip_Popup; + toolTip.Dispose(); + } + if (tb != null) + { + tb.KeyDown -= tb_KeyDown; + tb.KeyPressed -= tb_KeyPressed; + tb.SelectionChanged -= tb_SelectionChanged; + } + + if (timer != null) + { + timer.Stop(); + timer.Tick -= timer_Tick; + timer.Dispose(); + } + + base.Dispose(disposing); + } + + void SafetyClose() + { + if (Menu != null && !Menu.IsDisposed) + Menu.Close(); + } + + void tb_KeyPressed(object sender, KeyPressEventArgs e) + { + bool backspaceORdel = e.KeyChar == '\b' || e.KeyChar == 0xff; + + /* + if (backspaceORdel) + prevSelection = tb.Selection.Start;*/ + + if (Menu.Visible && !backspaceORdel) + DoAutocomplete(false); + else + ResetTimer(timer); + } + + void timer_Tick(object sender, EventArgs e) + { + timer.Stop(); + DoAutocomplete(false); + } + + void ResetTimer(System.Windows.Forms.Timer timer) + { + timer.Stop(); + timer.Start(); + } + + internal void DoAutocomplete() + { + DoAutocomplete(false); + } + + internal void DoAutocomplete(bool forced) + { + if (!Menu.Enabled) + { + Menu.Close(); + return; + } + + visibleItems.Clear(); + FocussedItemIndex = 0; + VerticalScroll.Value = 0; + //some magic for update scrolls + AutoScrollMinSize -= new Size(1, 0); + AutoScrollMinSize += new Size(1, 0); + //get fragment around caret + Range fragment = tb.Selection.GetFragment(Menu.SearchPattern); + string text = fragment.Text; + //calc screen point for popup menu + Point point = tb.PlaceToPoint(fragment.End); + point.Offset(2, tb.CharHeight); + // + if (forced || (text.Length >= Menu.MinFragmentLength + && tb.Selection.IsEmpty /*pops up only if selected range is empty*/ + && (tb.Selection.Start > fragment.Start || text.Length == 0/*pops up only if caret is after first letter*/))) + { + Menu.Fragment = fragment; + bool foundSelected = false; + //build popup menu + foreach (var item in sourceItems) + { + item.Parent = Menu; + CompareResult res = item.Compare(text); + if(res != CompareResult.Hidden) + visibleItems.Add(item); + if (res == CompareResult.VisibleAndSelected && !foundSelected) + { + foundSelected = true; + FocussedItemIndex = visibleItems.Count - 1; + } + } + + if (foundSelected) + { + AdjustScroll(); + DoSelectedVisible(); + } + } + + //show popup menu + if (Count > 0) + { + if (!Menu.Visible) + { + CancelEventArgs args = new CancelEventArgs(); + Menu.OnOpening(args); + if(!args.Cancel) + Menu.Show(tb, point); + } + + DoSelectedVisible(); + Invalidate(); + } + else + Menu.Close(); + } + + void tb_SelectionChanged(object sender, EventArgs e) + { + /* + FastColoredTextBox tb = sender as FastColoredTextBox; + + if (Math.Abs(prevSelection.iChar - tb.Selection.Start.iChar) > 1 || + prevSelection.iLine != tb.Selection.Start.iLine) + Menu.Close(); + prevSelection = tb.Selection.Start;*/ + if (Menu.Visible) + { + bool needClose = false; + + if (!tb.Selection.IsEmpty) + needClose = true; + else + if (!Menu.Fragment.Contains(tb.Selection.Start)) + { + if (tb.Selection.Start.iLine == Menu.Fragment.End.iLine && tb.Selection.Start.iChar == Menu.Fragment.End.iChar + 1) + { + //user press key at end of fragment + char c = tb.Selection.CharBeforeStart; + if (!Regex.IsMatch(c.ToString(), Menu.SearchPattern))//check char + needClose = true; + } + else + needClose = true; + } + + if (needClose) + Menu.Close(); + } + + } + + void tb_KeyDown(object sender, KeyEventArgs e) + { + var tb = sender as FastColoredTextBox; + + if (Menu.Visible) + if (ProcessKey(e.KeyCode, e.Modifiers)) + e.Handled = true; + + if (!Menu.Visible) + { + if (tb.HotkeysMapping.ContainsKey(e.KeyData) && tb.HotkeysMapping[e.KeyData] == FCTBAction.AutocompleteMenu) + { + DoAutocomplete(); + e.Handled = true; + } + else + { + if (e.KeyCode == Keys.Escape && timer.Enabled) + timer.Stop(); + } + } + } + + void AdjustScroll() + { + if (oldItemCount == visibleItems.Count) + return; + + int needHeight = ItemHeight * visibleItems.Count + 1; + Height = Math.Min(needHeight, MaximumSize.Height); + Menu.CalcSize(); + + AutoScrollMinSize = new Size(0, needHeight); + oldItemCount = visibleItems.Count; + } + + protected override void OnPaint(PaintEventArgs e) + { + AdjustScroll(); + + var itemHeight = ItemHeight; + int startI = VerticalScroll.Value / itemHeight - 1; + int finishI = (VerticalScroll.Value + ClientSize.Height) / itemHeight + 1; + startI = Math.Max(startI, 0); + finishI = Math.Min(finishI, visibleItems.Count); + int y = 0; + int leftPadding = 18; + for (int i = startI; i < finishI; i++) + { + y = i * itemHeight - VerticalScroll.Value; + + var item = visibleItems[i]; + + if(item.BackColor != Color.Transparent) + using (var brush = new SolidBrush(item.BackColor)) + e.Graphics.FillRectangle(brush, 1, y, ClientSize.Width - 1 - 1, itemHeight - 1); + + if (ImageList != null && visibleItems[i].ImageIndex >= 0) + e.Graphics.DrawImage(ImageList.Images[item.ImageIndex], 1, y); + + if (i == FocussedItemIndex) + using (var selectedBrush = new LinearGradientBrush(new Point(0, y - 3), new Point(0, y + itemHeight), Color.Transparent, SelectedColor)) + using (var pen = new Pen(SelectedColor)) + { + e.Graphics.FillRectangle(selectedBrush, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight - 1); + e.Graphics.DrawRectangle(pen, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight - 1); + } + + if (i == hoveredItemIndex) + using(var pen = new Pen(HoveredColor)) + e.Graphics.DrawRectangle(pen, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight - 1); + + using (var brush = new SolidBrush(item.ForeColor != Color.Transparent ? item.ForeColor : ForeColor)) + e.Graphics.DrawString(item.ToString(), Font, brush, leftPadding, y); + } + } + + protected override void OnScroll(ScrollEventArgs se) + { + base.OnScroll(se); + Invalidate(); + } + + protected override void OnMouseClick(MouseEventArgs e) + { + base.OnMouseClick(e); + + if (e.Button == System.Windows.Forms.MouseButtons.Left) + { + FocussedItemIndex = PointToItemIndex(e.Location); + DoSelectedVisible(); + Invalidate(); + } + } + + protected override void OnMouseDoubleClick(MouseEventArgs e) + { + base.OnMouseDoubleClick(e); + FocussedItemIndex = PointToItemIndex(e.Location); + Invalidate(); + OnSelecting(); + } + + internal virtual void OnSelecting() + { + if (FocussedItemIndex < 0 || FocussedItemIndex >= visibleItems.Count) + return; + tb.TextSource.Manager.BeginAutoUndoCommands(); + try + { + AutocompleteItem item = FocussedItem; + SelectingEventArgs args = new SelectingEventArgs() + { + Item = item, + SelectedIndex = FocussedItemIndex + }; + + Menu.OnSelecting(args); + + if (args.Cancel) + { + FocussedItemIndex = args.SelectedIndex; + Invalidate(); + return; + } + + if (!args.Handled) + { + var fragment = Menu.Fragment; + DoAutocomplete(item, fragment); + } + + Menu.Close(); + // + SelectedEventArgs args2 = new SelectedEventArgs() + { + Item = item, + Tb = Menu.Fragment.tb + }; + item.OnSelected(Menu, args2); + Menu.OnSelected(args2); + } + finally + { + tb.TextSource.Manager.EndAutoUndoCommands(); + } + } + + private void DoAutocomplete(AutocompleteItem item, Range fragment) + { + string newText = item.GetTextForReplace(); + + //replace text of fragment + var tb = fragment.tb; + + tb.BeginAutoUndo(); + tb.TextSource.Manager.ExecuteCommand(new SelectCommand(tb.TextSource)); + if (tb.Selection.ColumnSelectionMode) + { + var start = tb.Selection.Start; + var end = tb.Selection.End; + start.iChar = fragment.Start.iChar; + end.iChar = fragment.End.iChar; + tb.Selection.Start = start; + tb.Selection.End = end; + } + else + { + tb.Selection.Start = fragment.Start; + tb.Selection.End = fragment.End; + } + tb.InsertText(newText); + tb.TextSource.Manager.ExecuteCommand(new SelectCommand(tb.TextSource)); + tb.EndAutoUndo(); + tb.Focus(); + } + + int PointToItemIndex(Point p) + { + return (p.Y + VerticalScroll.Value) / ItemHeight; + } + + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + ProcessKey(keyData, Keys.None); + + return base.ProcessCmdKey(ref msg, keyData); + } + + private bool ProcessKey(Keys keyData, Keys keyModifiers) + { + if (keyModifiers == Keys.None) + switch (keyData) + { + case Keys.Down: + SelectNext(+1); + return true; + case Keys.PageDown: + SelectNext(+10); + return true; + case Keys.Up: + SelectNext(-1); + return true; + case Keys.PageUp: + SelectNext(-10); + return true; + case Keys.Enter: + OnSelecting(); + return true; + case Keys.Tab: + if (!AllowTabKey) + break; + OnSelecting(); + return true; + case Keys.Escape: + Menu.Close(); + return true; + } + + return false; + } + + public void SelectNext(int shift) + { + FocussedItemIndex = Math.Max(0, Math.Min(FocussedItemIndex + shift, visibleItems.Count - 1)); + DoSelectedVisible(); + // + Invalidate(); + } + + private void DoSelectedVisible() + { + if (FocussedItem != null) + SetToolTip(FocussedItem); + + var y = FocussedItemIndex * ItemHeight - VerticalScroll.Value; + if (y < 0) + VerticalScroll.Value = FocussedItemIndex * ItemHeight; + if (y > ClientSize.Height - ItemHeight) + VerticalScroll.Value = Math.Min(VerticalScroll.Maximum, FocussedItemIndex * ItemHeight - ClientSize.Height + ItemHeight); + //some magic for update scrolls + AutoScrollMinSize -= new Size(1, 0); + AutoScrollMinSize += new Size(1, 0); + } + + private void SetToolTip(AutocompleteItem autocompleteItem) + { + var title = autocompleteItem.ToolTipTitle; + var text = autocompleteItem.ToolTipText; + + if (string.IsNullOrEmpty(title)) + { + toolTip.ToolTipTitle = null; + toolTip.SetToolTip(this, null); + return; + } + + if (this.Parent != null) + { + IWin32Window window = this.Parent ?? this; + Point location; + + if ((this.PointToScreen(this.Location).X + MaxToolTipSize.Width + 105) < Screen.FromControl(this.Parent).WorkingArea.Right) + location = new Point(Right + 5, 0); + else + location = new Point(Left - 105 - MaximumSize.Width, 0); + + if (string.IsNullOrEmpty(text)) + { + toolTip.ToolTipTitle = null; + toolTip.Show(title, window, location.X, location.Y, ToolTipDuration); + } + else + { + toolTip.ToolTipTitle = title; + toolTip.Show(text, window, location.X, location.Y, ToolTipDuration); + } + } + } + + public int Count + { + get { return visibleItems.Count; } + } + + public void SetAutocompleteItems(ICollection items) + { + List list = new List(items.Count); + foreach (var item in items) + list.Add(new AutocompleteItem(item)); + SetAutocompleteItems(list); + } + + public void SetAutocompleteItems(IEnumerable items) + { + sourceItems = items; + } + } + + public class SelectingEventArgs : EventArgs + { + public AutocompleteItem Item { get; internal set; } + public bool Cancel {get;set;} + public int SelectedIndex{get;set;} + public bool Handled { get; set; } + } + + public class SelectedEventArgs : EventArgs + { + public AutocompleteItem Item { get; internal set; } + public FastColoredTextBox Tb { get; set; } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Bookmarks.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Bookmarks.cs new file mode 100644 index 0000000..ad220d9 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Bookmarks.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Text; + +namespace FastColoredTextBoxNS +{ + /// + /// Base class for bookmark collection + /// + public abstract class BaseBookmarks : ICollection, IDisposable + { + #region ICollection + public abstract void Add(Bookmark item); + public abstract void Clear(); + public abstract bool Contains(Bookmark item); + public abstract void CopyTo(Bookmark[] array, int arrayIndex); + public abstract int Count { get; } + public abstract bool IsReadOnly { get; } + public abstract bool Remove(Bookmark item); + public abstract IEnumerator GetEnumerator(); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region IDisposable + public abstract void Dispose(); + #endregion + + #region Additional properties + + public abstract void Add(int lineIndex, string bookmarkName); + public abstract void Add(int lineIndex); + public abstract bool Contains(int lineIndex); + public abstract bool Remove(int lineIndex); + public abstract Bookmark GetBookmark(int i); + + #endregion + } + + /// + /// Collection of bookmarks + /// + public class Bookmarks : BaseBookmarks + { + protected FastColoredTextBox tb; + protected List items = new List(); + protected int counter; + + public Bookmarks(FastColoredTextBox tb) + { + this.tb = tb; + tb.LineInserted += tb_LineInserted; + tb.LineRemoved += tb_LineRemoved; + } + + protected virtual void tb_LineRemoved(object sender, LineRemovedEventArgs e) + { + for(int i=0; i= e.Index) + { + if (items[i].LineIndex >= e.Index + e.Count) + { + items[i].LineIndex = items[i].LineIndex - e.Count; + continue; + } + + var was = e.Index <= 0; + foreach (var b in items) + if (b.LineIndex == e.Index - 1) + was = true; + + if(was) + { + items.RemoveAt(i); + i--; + }else + items[i].LineIndex = e.Index - 1; + + //if (items[i].LineIndex == e.Index + e.Count - 1) + //{ + // items[i].LineIndex = items[i].LineIndex - e.Count; + // continue; + //} + // + //items.RemoveAt(i); + //i--; + } + } + + protected virtual void tb_LineInserted(object sender, LineInsertedEventArgs e) + { + for (int i = 0; i < Count; i++) + if (items[i].LineIndex >= e.Index) + { + items[i].LineIndex = items[i].LineIndex + e.Count; + }else + if (items[i].LineIndex == e.Index - 1 && e.Count == 1) + { + if(tb[e.Index - 1].StartSpacesCount == tb[e.Index - 1].Count) + items[i].LineIndex = items[i].LineIndex + e.Count; + } + } + + public override void Dispose() + { + tb.LineInserted -= tb_LineInserted; + tb.LineRemoved -= tb_LineRemoved; + } + + public override IEnumerator GetEnumerator() + { + foreach (var item in items) + yield return item; + } + + public override void Add(int lineIndex, string bookmarkName) + { + Add(new Bookmark(tb, bookmarkName ?? "Bookmark " + counter, lineIndex)); + } + + public override void Add(int lineIndex) + { + Add(new Bookmark(tb, "Bookmark " + counter, lineIndex)); + } + + public override void Clear() + { + items.Clear(); + counter = 0; + } + + public override void Add(Bookmark bookmark) + { + foreach (var bm in items) + if (bm.LineIndex == bookmark.LineIndex) + return; + + items.Add(bookmark); + counter++; + tb.Invalidate(); + } + + public override bool Contains(Bookmark item) + { + return items.Contains(item); + } + + public override bool Contains(int lineIndex) + { + foreach (var item in items) + if (item.LineIndex == lineIndex) + return true; + return false; + } + + public override void CopyTo(Bookmark[] array, int arrayIndex) + { + items.CopyTo(array, arrayIndex); + } + + public override int Count + { + get { return items.Count; } + } + + public override bool IsReadOnly + { + get { return false; } + } + + public override bool Remove(Bookmark item) + { + tb.Invalidate(); + return items.Remove(item); + } + + /// + /// Removes bookmark by line index + /// + public override bool Remove(int lineIndex) + { + bool was = false; + for (int i = 0; i < Count; i++) + if (items[i].LineIndex == lineIndex) + { + items.RemoveAt(i); + i--; + was = true; + } + tb.Invalidate(); + + return was; + } + + /// + /// Returns Bookmark by index. + /// + public override Bookmark GetBookmark(int i) + { + return items[i]; + } + } + + /// + /// Bookmark of FastColoredTextbox + /// + public class Bookmark + { + public FastColoredTextBox TB { get; private set; } + /// + /// Name of bookmark + /// + public string Name { get; set; } + /// + /// Line index + /// + public int LineIndex {get; set; } + /// + /// Color of bookmark sign + /// + public Color Color { get; set; } + + /// + /// Scroll textbox to the bookmark + /// + public virtual void DoVisible() + { + TB.Selection.Start = new Place(0, LineIndex); + TB.DoRangeVisible(TB.Selection, true); + TB.Invalidate(); + } + + public Bookmark(FastColoredTextBox tb, string name, int lineIndex) + { + this.TB = tb; + this.Name = name; + this.LineIndex = lineIndex; + Color = tb.BookmarkColor; + } + + public virtual void Paint(Graphics gr, Rectangle lineRect) + { + var size = TB.CharHeight - 1; + using (var brush = new LinearGradientBrush(new Rectangle(0, lineRect.Top, size, size), Color.White, Color, 45)) + gr.FillEllipse(brush, 0, lineRect.Top, size, size); + using (var pen = new Pen(Color)) + gr.DrawEllipse(pen, 0, lineRect.Top, size, size); + } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Char.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Char.cs new file mode 100644 index 0000000..9f848c9 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Char.cs @@ -0,0 +1,26 @@ +using System; + +namespace FastColoredTextBoxNS +{ + /// + /// Char and style + /// + public struct Char + { + /// + /// Unicode character + /// + public char c; + /// + /// Style bit mask + /// + /// Bit 1 in position n means that this char will rendering by FastColoredTextBox.Styles[n] + public StyleIndex style; + + public Char(char c) + { + this.c = c; + style = StyleIndex.None; + } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/CommandManager.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/CommandManager.cs new file mode 100644 index 0000000..ce1e1bf --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/CommandManager.cs @@ -0,0 +1,245 @@ +using System.Collections.Generic; +using System; + +namespace FastColoredTextBoxNS +{ + public class CommandManager + { + public static int MaxHistoryLength = 200; + + LimitedStack history; + Stack redoStack = new Stack(); + public TextSource TextSource{ get; private set; } + public bool UndoRedoStackIsEnabled { get; set; } + + public event EventHandler RedoCompleted = delegate { }; + + public CommandManager(TextSource ts) + { + history = new LimitedStack(MaxHistoryLength); + TextSource = ts; + UndoRedoStackIsEnabled = true; + } + + public virtual void ExecuteCommand(Command cmd) + { + if (disabledCommands > 0) + return; + + //multirange ? + if (cmd.ts.CurrentTB.Selection.ColumnSelectionMode) + if (cmd is UndoableCommand) + //make wrapper + cmd = new MultiRangeCommand((UndoableCommand)cmd); + + + if (cmd is UndoableCommand) + { + //if range is ColumnRange, then create wrapper + (cmd as UndoableCommand).autoUndo = autoUndoCommands > 0; + history.Push(cmd as UndoableCommand); + } + + try + { + cmd.Execute(); + } + catch (ArgumentOutOfRangeException) + { + //OnTextChanging cancels enter of the text + if (cmd is UndoableCommand) + history.Pop(); + } + // + if (!UndoRedoStackIsEnabled) + ClearHistory(); + // + redoStack.Clear(); + // + TextSource.CurrentTB.OnUndoRedoStateChanged(); + } + + public void Undo() + { + if (history.Count > 0) + { + var cmd = history.Pop(); + // + BeginDisableCommands();//prevent text changing into handlers + try + { + cmd.Undo(); + } + finally + { + EndDisableCommands(); + } + // + redoStack.Push(cmd); + } + + //undo next autoUndo command + if (history.Count > 0) + { + if (history.Peek().autoUndo) + Undo(); + } + + TextSource.CurrentTB.OnUndoRedoStateChanged(); + } + + protected int disabledCommands = 0; + + private void EndDisableCommands() + { + disabledCommands--; + } + + private void BeginDisableCommands() + { + disabledCommands++; + } + + int autoUndoCommands = 0; + + public void EndAutoUndoCommands() + { + autoUndoCommands--; + if (autoUndoCommands == 0) + if (history.Count > 0) + history.Peek().autoUndo = false; + } + + public void BeginAutoUndoCommands() + { + autoUndoCommands++; + } + + internal void ClearHistory() + { + history.Clear(); + redoStack.Clear(); + TextSource.CurrentTB.OnUndoRedoStateChanged(); + } + + internal void Redo() + { + if (redoStack.Count == 0) + return; + UndoableCommand cmd; + BeginDisableCommands();//prevent text changing into handlers + try + { + cmd = redoStack.Pop(); + if (TextSource.CurrentTB.Selection.ColumnSelectionMode) + TextSource.CurrentTB.Selection.ColumnSelectionMode = false; + TextSource.CurrentTB.Selection.Start = cmd.sel.Start; + TextSource.CurrentTB.Selection.End = cmd.sel.End; + cmd.Execute(); + history.Push(cmd); + } + finally + { + EndDisableCommands(); + } + + //call event + RedoCompleted(this, EventArgs.Empty); + + //redo command after autoUndoable command + if (cmd.autoUndo) + Redo(); + + TextSource.CurrentTB.OnUndoRedoStateChanged(); + } + + public bool UndoEnabled + { + get + { + return history.Count > 0; + } + } + + public bool RedoEnabled + { + get + { + return redoStack.Count > 0; + } + } + } + + public abstract class Command + { + public TextSource ts; + public abstract void Execute(); + } + + internal class RangeInfo + { + public Place Start { get; set; } + public Place End { get; set; } + + public RangeInfo(Range r) + { + Start = r.Start; + End = r.End; + } + + internal int FromX + { + get + { + if (End.iLine < Start.iLine) return End.iChar; + if (End.iLine > Start.iLine) return Start.iChar; + return Math.Min(End.iChar, Start.iChar); + } + } + } + + public abstract class UndoableCommand : Command + { + internal RangeInfo sel; + internal RangeInfo lastSel; + internal bool autoUndo; + + public UndoableCommand(TextSource ts) + { + this.ts = ts; + sel = new RangeInfo(ts.CurrentTB.Selection); + } + + public virtual void Undo() + { + OnTextChanged(true); + } + + public override void Execute() + { + lastSel = new RangeInfo(ts.CurrentTB.Selection); + OnTextChanged(false); + } + + protected virtual void OnTextChanged(bool invert) + { + bool b = sel.Start.iLine < lastSel.Start.iLine; + if (invert) + { + if (b) + ts.OnTextChanged(sel.Start.iLine, sel.Start.iLine); + else + ts.OnTextChanged(sel.Start.iLine, lastSel.Start.iLine); + } + else + { + if (b) + ts.OnTextChanged(sel.Start.iLine, lastSel.Start.iLine); + else + ts.OnTextChanged(lastSel.Start.iLine, lastSel.Start.iLine); + } + } + + public abstract UndoableCommand Clone(); + } +} \ No newline at end of file diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Commands.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Commands.cs new file mode 100644 index 0000000..c83ac96 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/Commands.cs @@ -0,0 +1,809 @@ +using System; +using System.Collections.Generic; + +namespace FastColoredTextBoxNS +{ + /// + /// Insert single char + /// + /// This operation includes also insertion of new line and removing char by backspace + public class InsertCharCommand : UndoableCommand + { + public char c; + char deletedChar = '\x0'; + + /// + /// Constructor + /// + /// Underlaying textbox + /// Inserting char + public InsertCharCommand(TextSource ts, char c): base(ts) + { + this.c = c; + } + + /// + /// Undo operation + /// + public override void Undo() + { + ts.OnTextChanging(); + switch (c) + { + case '\n': MergeLines(sel.Start.iLine, ts); break; + case '\r': break; + case '\b': + ts.CurrentTB.Selection.Start = lastSel.Start; + char cc = '\x0'; + if (deletedChar != '\x0') + { + ts.CurrentTB.ExpandBlock(ts.CurrentTB.Selection.Start.iLine); + InsertChar(deletedChar, ref cc, ts); + } + break; + case '\t': + ts.CurrentTB.ExpandBlock(sel.Start.iLine); + for (int i = sel.FromX; i < lastSel.FromX; i++) + ts[sel.Start.iLine].RemoveAt(sel.Start.iChar); + ts.CurrentTB.Selection.Start = sel.Start; + break; + default: + ts.CurrentTB.ExpandBlock(sel.Start.iLine); + ts[sel.Start.iLine].RemoveAt(sel.Start.iChar); + ts.CurrentTB.Selection.Start = sel.Start; + break; + } + + ts.NeedRecalc(new TextSource.TextChangedEventArgs(sel.Start.iLine, sel.Start.iLine)); + + base.Undo(); + } + + /// + /// Execute operation + /// + public override void Execute() + { + ts.CurrentTB.ExpandBlock(ts.CurrentTB.Selection.Start.iLine); + string s = c.ToString(); + ts.OnTextChanging(ref s); + if (s.Length == 1) + c = s[0]; + + if (String.IsNullOrEmpty(s)) + throw new ArgumentOutOfRangeException(); + + + if (ts.Count == 0) + InsertLine(ts); + InsertChar(c, ref deletedChar, ts); + + ts.NeedRecalc(new TextSource.TextChangedEventArgs(ts.CurrentTB.Selection.Start.iLine, ts.CurrentTB.Selection.Start.iLine)); + base.Execute(); + } + + internal static void InsertChar(char c, ref char deletedChar, TextSource ts) + { + var tb = ts.CurrentTB; + + switch (c) + { + case '\n': + if (!ts.CurrentTB.AllowInsertRemoveLines) + throw new ArgumentOutOfRangeException("Cant insert this char in ColumnRange mode"); + if (ts.Count == 0) + InsertLine(ts); + InsertLine(ts); + break; + case '\r': break; + case '\b'://backspace + if (tb.Selection.Start.iChar == 0 && tb.Selection.Start.iLine == 0) + return; + if (tb.Selection.Start.iChar == 0) + { + if (!ts.CurrentTB.AllowInsertRemoveLines) + throw new ArgumentOutOfRangeException("Cant insert this char in ColumnRange mode"); + if (tb.LineInfos[tb.Selection.Start.iLine - 1].VisibleState != VisibleState.Visible) + tb.ExpandBlock(tb.Selection.Start.iLine - 1); + deletedChar = '\n'; + MergeLines(tb.Selection.Start.iLine - 1, ts); + } + else + { + deletedChar = ts[tb.Selection.Start.iLine][tb.Selection.Start.iChar - 1].c; + ts[tb.Selection.Start.iLine].RemoveAt(tb.Selection.Start.iChar - 1); + tb.Selection.Start = new Place(tb.Selection.Start.iChar - 1, tb.Selection.Start.iLine); + } + break; + case '\t': + int spaceCountNextTabStop = tb.TabLength - (tb.Selection.Start.iChar % tb.TabLength); + if (spaceCountNextTabStop == 0) + spaceCountNextTabStop = tb.TabLength; + + for (int i = 0; i < spaceCountNextTabStop; i++) + ts[tb.Selection.Start.iLine].Insert(tb.Selection.Start.iChar, new Char(' ')); + + tb.Selection.Start = new Place(tb.Selection.Start.iChar + spaceCountNextTabStop, tb.Selection.Start.iLine); + break; + default: + ts[tb.Selection.Start.iLine].Insert(tb.Selection.Start.iChar, new Char(c)); + tb.Selection.Start = new Place(tb.Selection.Start.iChar + 1, tb.Selection.Start.iLine); + break; + } + } + + internal static void InsertLine(TextSource ts) + { + var tb = ts.CurrentTB; + + if (!tb.Multiline && tb.LinesCount > 0) + return; + + if (ts.Count == 0) + ts.InsertLine(0, ts.CreateLine()); + else + BreakLines(tb.Selection.Start.iLine, tb.Selection.Start.iChar, ts); + + tb.Selection.Start = new Place(0, tb.Selection.Start.iLine + 1); + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + } + + /// + /// Merge lines i and i+1 + /// + internal static void MergeLines(int i, TextSource ts) + { + var tb = ts.CurrentTB; + + if (i + 1 >= ts.Count) + return; + tb.ExpandBlock(i); + tb.ExpandBlock(i + 1); + int pos = ts[i].Count; + // + /* + if(ts[i].Count == 0) + ts.RemoveLine(i); + else*/ + if (ts[i + 1].Count == 0) + ts.RemoveLine(i + 1); + else + { + ts[i].AddRange(ts[i + 1]); + ts.RemoveLine(i + 1); + } + tb.Selection.Start = new Place(pos, i); + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + } + + internal static void BreakLines(int iLine, int pos, TextSource ts) + { + Line newLine = ts.CreateLine(); + for(int i=pos;i + /// Insert text + /// + public class InsertTextCommand : UndoableCommand + { + public string InsertedText; + + /// + /// Constructor + /// + /// Underlaying textbox + /// Text for inserting + public InsertTextCommand(TextSource ts, string insertedText): base(ts) + { + this.InsertedText = insertedText; + } + + /// + /// Undo operation + /// + public override void Undo() + { + ts.CurrentTB.Selection.Start = sel.Start; + ts.CurrentTB.Selection.End = lastSel.Start; + ts.OnTextChanging(); + ClearSelectedCommand.ClearSelected(ts); + base.Undo(); + } + + /// + /// Execute operation + /// + public override void Execute() + { + ts.OnTextChanging(ref InsertedText); + InsertText(InsertedText, ts); + base.Execute(); + } + + internal static void InsertText(string insertedText, TextSource ts) + { + var tb = ts.CurrentTB; + try + { + tb.Selection.BeginUpdate(); + char cc = '\x0'; + + if (ts.Count == 0) + { + InsertCharCommand.InsertLine(ts); + tb.Selection.Start = Place.Empty; + } + tb.ExpandBlock(tb.Selection.Start.iLine); + var len = insertedText.Length; + for (int i = 0; i < len; i++) + { + var c = insertedText[i]; + if(c == '\r' && (i >= len - 1 || insertedText[i + 1] != '\n')) + InsertCharCommand.InsertChar('\n', ref cc, ts); + else + InsertCharCommand.InsertChar(c, ref cc, ts); + } + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + } + finally { + tb.Selection.EndUpdate(); + } + } + + public override UndoableCommand Clone() + { + return new InsertTextCommand(ts, InsertedText); + } + } + + /// + /// Insert text into given ranges + /// + public class ReplaceTextCommand : UndoableCommand + { + string insertedText; + List ranges; + List prevText = new List(); + + /// + /// Constructor + /// + /// Underlaying textbox + /// List of ranges for replace + /// Text for inserting + public ReplaceTextCommand(TextSource ts, List ranges, string insertedText) + : base(ts) + { + //sort ranges by place + ranges.Sort((r1, r2)=> + { + if (r1.Start.iLine == r2.Start.iLine) + return r1.Start.iChar.CompareTo(r2.Start.iChar); + return r1.Start.iLine.CompareTo(r2.Start.iLine); + }); + // + this.ranges = ranges; + this.insertedText = insertedText; + lastSel = sel = new RangeInfo(ts.CurrentTB.Selection); + } + + /// + /// Undo operation + /// + public override void Undo() + { + var tb = ts.CurrentTB; + + ts.OnTextChanging(); + tb.BeginUpdate(); + + tb.Selection.BeginUpdate(); + for (int i = 0; i 0) + ts.OnTextChanged(ranges[0].Start.iLine, ranges[ranges.Count - 1].End.iLine); + + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + } + + /// + /// Execute operation + /// + public override void Execute() + { + var tb = ts.CurrentTB; + prevText.Clear(); + + ts.OnTextChanging(ref insertedText); + + tb.Selection.BeginUpdate(); + tb.BeginUpdate(); + for (int i = ranges.Count - 1; i >= 0; i--) + { + tb.Selection.Start = ranges[i].Start; + tb.Selection.End = ranges[i].End; + prevText.Add(tb.Selection.Text); + ClearSelected(ts); + if (insertedText != "") + InsertTextCommand.InsertText(insertedText, ts); + } + if(ranges.Count > 0) + ts.OnTextChanged(ranges[0].Start.iLine, ranges[ranges.Count - 1].End.iLine); + tb.EndUpdate(); + tb.Selection.EndUpdate(); + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + + lastSel = new RangeInfo(tb.Selection); + } + + public override UndoableCommand Clone() + { + return new ReplaceTextCommand(ts, new List(ranges), insertedText); + } + + internal static void ClearSelected(TextSource ts) + { + var tb = ts.CurrentTB; + + tb.Selection.Normalize(); + + Place start = tb.Selection.Start; + Place end = tb.Selection.End; + int fromLine = Math.Min(end.iLine, start.iLine); + int toLine = Math.Max(end.iLine, start.iLine); + int fromChar = tb.Selection.FromX; + int toChar = tb.Selection.ToX; + if (fromLine < 0) return; + // + if (fromLine == toLine) + ts[fromLine].RemoveRange(fromChar, toChar - fromChar); + else + { + ts[fromLine].RemoveRange(fromChar, ts[fromLine].Count - fromChar); + ts[toLine].RemoveRange(0, toChar); + ts.RemoveLine(fromLine + 1, toLine - fromLine - 1); + InsertCharCommand.MergeLines(fromLine, ts); + } + } + } + + /// + /// Clear selected text + /// + public class ClearSelectedCommand : UndoableCommand + { + string deletedText; + + /// + /// Construstor + /// + /// Underlaying textbox + public ClearSelectedCommand(TextSource ts): base(ts) + { + } + + /// + /// Undo operation + /// + public override void Undo() + { + ts.CurrentTB.Selection.Start = new Place(sel.FromX, Math.Min(sel.Start.iLine, sel.End.iLine)); + ts.OnTextChanging(); + InsertTextCommand.InsertText(deletedText, ts); + ts.OnTextChanged(sel.Start.iLine, sel.End.iLine); + ts.CurrentTB.Selection.Start = sel.Start; + ts.CurrentTB.Selection.End = sel.End; + } + + /// + /// Execute operation + /// + public override void Execute() + { + var tb = ts.CurrentTB; + + string temp = null; + ts.OnTextChanging(ref temp); + if (temp == "") + throw new ArgumentOutOfRangeException(); + + deletedText = tb.Selection.Text; + ClearSelected(ts); + lastSel = new RangeInfo(tb.Selection); + ts.OnTextChanged(lastSel.Start.iLine, lastSel.Start.iLine); + } + + internal static void ClearSelected(TextSource ts) + { + var tb = ts.CurrentTB; + + Place start = tb.Selection.Start; + Place end = tb.Selection.End; + int fromLine = Math.Min(end.iLine, start.iLine); + int toLine = Math.Max(end.iLine, start.iLine); + int fromChar = tb.Selection.FromX; + int toChar = tb.Selection.ToX; + if (fromLine < 0) return; + // + if (fromLine == toLine) + ts[fromLine].RemoveRange(fromChar, toChar - fromChar); + else + { + ts[fromLine].RemoveRange(fromChar, ts[fromLine].Count - fromChar); + ts[toLine].RemoveRange(0, toChar); + ts.RemoveLine(fromLine + 1, toLine - fromLine - 1); + InsertCharCommand.MergeLines(fromLine, ts); + } + // + tb.Selection.Start = new Place(fromChar, fromLine); + // + ts.NeedRecalc(new TextSource.TextChangedEventArgs(fromLine, toLine)); + } + + public override UndoableCommand Clone() + { + return new ClearSelectedCommand(ts); + } + } + + /// + /// Replaces text + /// + public class ReplaceMultipleTextCommand : UndoableCommand + { + List ranges; + List prevText = new List(); + + public class ReplaceRange + { + public Range ReplacedRange { get; set; } + public String ReplaceText { get; set; } + } + + /// + /// Constructor + /// + /// Underlaying textsource + /// List of ranges for replace + public ReplaceMultipleTextCommand(TextSource ts, List ranges) + : base(ts) + { + //sort ranges by place + ranges.Sort((r1, r2) => + { + if (r1.ReplacedRange.Start.iLine == r2.ReplacedRange.Start.iLine) + return r1.ReplacedRange.Start.iChar.CompareTo(r2.ReplacedRange.Start.iChar); + return r1.ReplacedRange.Start.iLine.CompareTo(r2.ReplacedRange.Start.iLine); + }); + // + this.ranges = ranges; + lastSel = sel = new RangeInfo(ts.CurrentTB.Selection); + } + + /// + /// Undo operation + /// + public override void Undo() + { + var tb = ts.CurrentTB; + + ts.OnTextChanging(); + + tb.Selection.BeginUpdate(); + for (int i = 0; i < ranges.Count; i++) + { + tb.Selection.Start = ranges[i].ReplacedRange.Start; + for (int j = 0; j < ranges[i].ReplaceText.Length; j++) + tb.Selection.GoRight(true); + ClearSelectedCommand.ClearSelected(ts); + var prevTextIndex = ranges.Count - 1 - i; + InsertTextCommand.InsertText(prevText[prevTextIndex], ts); + ts.OnTextChanged(ranges[i].ReplacedRange.Start.iLine, ranges[i].ReplacedRange.Start.iLine); + } + tb.Selection.EndUpdate(); + + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + } + + /// + /// Execute operation + /// + public override void Execute() + { + var tb = ts.CurrentTB; + prevText.Clear(); + + ts.OnTextChanging(); + + tb.Selection.BeginUpdate(); + for (int i = ranges.Count - 1; i >= 0; i--) + { + tb.Selection.Start = ranges[i].ReplacedRange.Start; + tb.Selection.End = ranges[i].ReplacedRange.End; + prevText.Add(tb.Selection.Text); + ClearSelectedCommand.ClearSelected(ts); + InsertTextCommand.InsertText(ranges[i].ReplaceText, ts); + ts.OnTextChanged(ranges[i].ReplacedRange.Start.iLine, ranges[i].ReplacedRange.End.iLine); + } + tb.Selection.EndUpdate(); + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + + lastSel = new RangeInfo(tb.Selection); + } + + public override UndoableCommand Clone() + { + return new ReplaceMultipleTextCommand(ts, new List(ranges)); + } + } + + /// + /// Removes lines + /// + public class RemoveLinesCommand : UndoableCommand + { + List iLines; + List prevText = new List(); + + /// + /// Constructor + /// + /// Underlaying textbox + /// List of ranges for replace + /// Text for inserting + public RemoveLinesCommand(TextSource ts, List iLines) + : base(ts) + { + //sort iLines + iLines.Sort(); + // + this.iLines = iLines; + lastSel = sel = new RangeInfo(ts.CurrentTB.Selection); + } + + /// + /// Undo operation + /// + public override void Undo() + { + var tb = ts.CurrentTB; + + ts.OnTextChanging(); + + tb.Selection.BeginUpdate(); + //tb.BeginUpdate(); + for (int i = 0; i < iLines.Count; i++) + { + var iLine = iLines[i]; + + if(iLine < ts.Count) + tb.Selection.Start = new Place(0, iLine); + else + tb.Selection.Start = new Place(ts[ts.Count - 1].Count, ts.Count - 1); + + InsertCharCommand.InsertLine(ts); + tb.Selection.Start = new Place(0, iLine); + var text = prevText[prevText.Count - i - 1]; + InsertTextCommand.InsertText(text, ts); + ts[iLine].IsChanged = true; + if (iLine < ts.Count - 1) + ts[iLine + 1].IsChanged = true; + else + ts[iLine - 1].IsChanged = true; + if(text.Trim() != string.Empty) + ts.OnTextChanged(iLine, iLine); + } + //tb.EndUpdate(); + tb.Selection.EndUpdate(); + + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + } + + /// + /// Execute operation + /// + public override void Execute() + { + var tb = ts.CurrentTB; + prevText.Clear(); + + ts.OnTextChanging(); + + tb.Selection.BeginUpdate(); + for(int i = iLines.Count - 1; i >= 0; i--) + { + var iLine = iLines[i]; + + prevText.Add(ts[iLine].Text);//backward + ts.RemoveLine(iLine); + //ts.OnTextChanged(ranges[i].Start.iLine, ranges[i].End.iLine); + } + tb.Selection.Start = new Place(0, 0); + tb.Selection.EndUpdate(); + ts.NeedRecalc(new TextSource.TextChangedEventArgs(0, 1)); + + lastSel = new RangeInfo(tb.Selection); + } + + public override UndoableCommand Clone() + { + return new RemoveLinesCommand(ts, new List(iLines)); + } + } + + /// + /// Wrapper for multirange commands + /// + public class MultiRangeCommand : UndoableCommand + { + private UndoableCommand cmd; + private Range range; + private List commandsByRanges = new List(); + + public MultiRangeCommand(UndoableCommand command):base(command.ts) + { + this.cmd = command; + range = ts.CurrentTB.Selection.Clone(); + } + + public override void Execute() + { + commandsByRanges.Clear(); + var prevSelection = range.Clone(); + var iChar = -1; + var iStartLine = prevSelection.Start.iLine; + var iEndLine = prevSelection.End.iLine; + ts.CurrentTB.Selection.ColumnSelectionMode = false; + ts.CurrentTB.Selection.BeginUpdate(); + ts.CurrentTB.BeginUpdate(); + ts.CurrentTB.AllowInsertRemoveLines = false; + try + { + if (cmd is InsertTextCommand) + ExecuteInsertTextCommand(ref iChar, (cmd as InsertTextCommand).InsertedText); + else + if (cmd is InsertCharCommand && (cmd as InsertCharCommand).c != '\x0' && (cmd as InsertCharCommand).c != '\b')//if not DEL or BACKSPACE + ExecuteInsertTextCommand(ref iChar, (cmd as InsertCharCommand).c.ToString()); + else + ExecuteCommand(ref iChar); + } + catch (ArgumentOutOfRangeException) + { + } + finally + { + ts.CurrentTB.AllowInsertRemoveLines = true; + ts.CurrentTB.EndUpdate(); + + ts.CurrentTB.Selection = range; + if (iChar >= 0) + { + ts.CurrentTB.Selection.Start = new Place(iChar, iStartLine); + ts.CurrentTB.Selection.End = new Place(iChar, iEndLine); + } + ts.CurrentTB.Selection.ColumnSelectionMode = true; + ts.CurrentTB.Selection.EndUpdate(); + } + } + + private void ExecuteInsertTextCommand(ref int iChar, string text) + { + var lines = text.Split('\n'); + var iLine = 0; + foreach (var r in range.GetSubRanges(true)) + { + var line = ts.CurrentTB[r.Start.iLine]; + var lineIsEmpty = r.End < r.Start && line.StartSpacesCount == line.Count; + if (!lineIsEmpty) + { + var insertedText = lines[iLine%lines.Length]; + if (r.End < r.Start && insertedText!="") + { + //add forwarding spaces + insertedText = new string(' ', r.Start.iChar - r.End.iChar) + insertedText; + r.Start = r.End; + } + ts.CurrentTB.Selection = r; + var c = new InsertTextCommand(ts, insertedText); + c.Execute(); + if (ts.CurrentTB.Selection.End.iChar > iChar) + iChar = ts.CurrentTB.Selection.End.iChar; + commandsByRanges.Add(c); + } + iLine++; + } + } + + private void ExecuteCommand(ref int iChar) + { + foreach (var r in range.GetSubRanges(false)) + { + ts.CurrentTB.Selection = r; + var c = cmd.Clone(); + c.Execute(); + if (ts.CurrentTB.Selection.End.iChar > iChar) + iChar = ts.CurrentTB.Selection.End.iChar; + commandsByRanges.Add(c); + } + } + + public override void Undo() + { + ts.CurrentTB.BeginUpdate(); + ts.CurrentTB.Selection.BeginUpdate(); + try + { + for (int i = commandsByRanges.Count - 1; i >= 0; i--) + commandsByRanges[i].Undo(); + } + finally + { + ts.CurrentTB.Selection.EndUpdate(); + ts.CurrentTB.EndUpdate(); + } + ts.CurrentTB.Selection = range.Clone(); + ts.CurrentTB.OnTextChanged(range); + ts.CurrentTB.OnSelectionChanged(); + ts.CurrentTB.Selection.ColumnSelectionMode = true; + } + + public override UndoableCommand Clone() + { + throw new NotImplementedException(); + } + } + + /// + /// Remembers current selection and restore it after Undo + /// + public class SelectCommand : UndoableCommand + { + public SelectCommand(TextSource ts):base(ts) + { + } + + public override void Execute() + { + //remember selection + lastSel = new RangeInfo(ts.CurrentTB.Selection); + } + + protected override void OnTextChanged(bool invert) + { + } + + public override void Undo() + { + //restore selection + ts.CurrentTB.Selection = new Range(ts.CurrentTB, lastSel.Start, lastSel.End); + } + + public override UndoableCommand Clone() + { + var result = new SelectCommand(ts); + if(lastSel!=null) + result.lastSel = new RangeInfo(new Range(ts.CurrentTB, lastSel.Start, lastSel.End)); + return result; + } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/DocumentMap.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/DocumentMap.cs new file mode 100644 index 0000000..ae519c6 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/DocumentMap.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Drawing.Drawing2D; +using System.Text; +using System.Windows.Forms; + +namespace FastColoredTextBoxNS +{ + /// + /// Shows document map of FCTB + /// + public class DocumentMap : Control + { + public EventHandler TargetChanged; + + FastColoredTextBox target; + private float scale = 0.3f; + private bool needRepaint = true; + private Place startPlace = Place.Empty; + private bool scrollbarVisible = true; + + [Description("Target FastColoredTextBox")] + public FastColoredTextBox Target + { + get { return target; } + set + { + if (target != null) + UnSubscribe(target); + + target = value; + if (value != null) + { + Subscribe(target); + } + OnTargetChanged(); + } + } + + /// + /// Scale + /// + [Description("Scale")] + [DefaultValue(0.3f)] + public new float Scale + { + get { return scale; } + set + { + scale = value; + NeedRepaint(); + } + } + + /// + /// Scrollbar visibility + /// + [Description("Scrollbar visibility")] + [DefaultValue(true)] + public bool ScrollbarVisible + { + get { return scrollbarVisible; } + set + { + scrollbarVisible = value; + NeedRepaint(); + } + } + + public DocumentMap() + { + ForeColor = Color.Maroon; + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true); + Application.Idle += Application_Idle; + } + + void Application_Idle(object sender, EventArgs e) + { + if(needRepaint) + Invalidate(); + } + + protected virtual void OnTargetChanged() + { + NeedRepaint(); + + if (TargetChanged != null) + TargetChanged(this, EventArgs.Empty); + } + + protected virtual void UnSubscribe(FastColoredTextBox target) + { + target.Scroll -= new ScrollEventHandler(Target_Scroll); + target.SelectionChangedDelayed -= new EventHandler(Target_SelectionChanged); + target.VisibleRangeChanged -= new EventHandler(Target_VisibleRangeChanged); + } + + protected virtual void Subscribe(FastColoredTextBox target) + { + target.Scroll += new ScrollEventHandler(Target_Scroll); + target.SelectionChangedDelayed += new EventHandler(Target_SelectionChanged); + target.VisibleRangeChanged += new EventHandler(Target_VisibleRangeChanged); + } + + protected virtual void Target_VisibleRangeChanged(object sender, EventArgs e) + { + NeedRepaint(); + } + + protected virtual void Target_SelectionChanged(object sender, EventArgs e) + { + NeedRepaint(); + } + + protected virtual void Target_Scroll(object sender, ScrollEventArgs e) + { + NeedRepaint(); + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + NeedRepaint(); + } + + public void NeedRepaint() + { + needRepaint = true; + } + + protected override void OnPaint(PaintEventArgs e) + { + if (target == null) + return; + + var zoom = this.Scale * 100 / target.Zoom; + + if (zoom <= float.Epsilon) + return; + + //calc startPlace + var r = target.VisibleRange; + if (startPlace.iLine > r.Start.iLine) + startPlace.iLine = r.Start.iLine; + else + { + var endP = target.PlaceToPoint(r.End); + endP.Offset(0, -(int)(ClientSize.Height / zoom) + target.CharHeight); + var pp = target.PointToPlace(endP); + if (pp.iLine > startPlace.iLine) + startPlace.iLine = pp.iLine; + } + startPlace.iChar = 0; + //calc scroll pos + var linesCount = target.Lines.Count; + var sp1 = (float)r.Start.iLine / linesCount; + var sp2 = (float)r.End.iLine / linesCount; + + //scale graphics + e.Graphics.ScaleTransform(zoom, zoom); + //draw text + var size = new SizeF(ClientSize.Width / zoom, ClientSize.Height / zoom); + target.DrawText(e.Graphics, startPlace, size.ToSize()); + + //draw visible rect + var p0 = target.PlaceToPoint(startPlace); + var p1 = target.PlaceToPoint(r.Start); + var p2 = target.PlaceToPoint(r.End); + var y1 = p1.Y - p0.Y; + var y2 = p2.Y + target.CharHeight - p0.Y; + + e.Graphics.SmoothingMode = SmoothingMode.HighQuality; + + using (var brush = new SolidBrush(Color.FromArgb(50, ForeColor))) + using (var pen = new Pen(brush, 1 / zoom)) + { + var rect = new Rectangle(0, y1, (int)((ClientSize.Width - 1) / zoom), y2 - y1); + e.Graphics.FillRectangle(brush, rect); + e.Graphics.DrawRectangle(pen, rect); + } + + //draw scrollbar + if (scrollbarVisible) + { + e.Graphics.ResetTransform(); + e.Graphics.SmoothingMode = SmoothingMode.None; + + using (var brush = new SolidBrush(Color.FromArgb(200, ForeColor))) + { + var rect = new RectangleF(ClientSize.Width - 3, ClientSize.Height*sp1, 2, + ClientSize.Height*(sp2 - sp1)); + e.Graphics.FillRectangle(brush, rect); + } + } + + needRepaint = false; + } + + protected override void OnMouseDown(MouseEventArgs e) + { + if (e.Button == System.Windows.Forms.MouseButtons.Left) + Scroll(e.Location); + base.OnMouseDown(e); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + if (e.Button == System.Windows.Forms.MouseButtons.Left) + Scroll(e.Location); + base.OnMouseMove(e); + } + + private void Scroll(Point point) + { + if (target == null) + return; + + var zoom = this.Scale*100/target.Zoom; + + if (zoom <= float.Epsilon) + return; + + var p0 = target.PlaceToPoint(startPlace); + p0 = new Point(0, p0.Y + (int) (point.Y/zoom)); + var pp = target.PointToPlace(p0); + target.DoRangeVisible(new Range(target, pp, pp), true); + BeginInvoke((MethodInvoker)OnScroll); + } + + private void OnScroll() + { + Refresh(); + target.Refresh(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Application.Idle -= Application_Idle; + if (target != null) + UnSubscribe(target); + } + base.Dispose(disposing); + } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/EncodingDetector.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/EncodingDetector.cs new file mode 100644 index 0000000..8a4e037 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/EncodingDetector.cs @@ -0,0 +1,363 @@ +// Copyright Tao Klerks, 2010-2012, tao@klerks.biz +// Licensed under the modified BSD license. + + +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace FastColoredTextBoxNS +{ + public static class EncodingDetector + { + const long _defaultHeuristicSampleSize = 0x10000; //completely arbitrary - inappropriate for high numbers of files / high speed requirements + + public static Encoding DetectTextFileEncoding(string InputFilename) + { + using (FileStream textfileStream = File.OpenRead(InputFilename)) + { + return DetectTextFileEncoding(textfileStream, _defaultHeuristicSampleSize); + } + } + + public static Encoding DetectTextFileEncoding(FileStream InputFileStream, long HeuristicSampleSize) + { + bool uselessBool = false; + return DetectTextFileEncoding(InputFileStream, _defaultHeuristicSampleSize, out uselessBool); + } + + public static Encoding DetectTextFileEncoding(FileStream InputFileStream, long HeuristicSampleSize, out bool HasBOM) + { + Encoding encodingFound = null; + + long originalPos = InputFileStream.Position; + + InputFileStream.Position = 0; + + + //First read only what we need for BOM detection + byte[] bomBytes = new byte[InputFileStream.Length > 4 ? 4 : InputFileStream.Length]; + InputFileStream.Read(bomBytes, 0, bomBytes.Length); + + encodingFound = DetectBOMBytes(bomBytes); + + if (encodingFound != null) + { + InputFileStream.Position = originalPos; + HasBOM = true; + return encodingFound; + } + + + //BOM Detection failed, going for heuristics now. + // create sample byte array and populate it + byte[] sampleBytes = new byte[HeuristicSampleSize > InputFileStream.Length ? InputFileStream.Length : HeuristicSampleSize]; + Array.Copy(bomBytes, sampleBytes, bomBytes.Length); + if (InputFileStream.Length > bomBytes.Length) + InputFileStream.Read(sampleBytes, bomBytes.Length, sampleBytes.Length - bomBytes.Length); + InputFileStream.Position = originalPos; + + //test byte array content + encodingFound = DetectUnicodeInByteSampleByHeuristics(sampleBytes); + + HasBOM = false; + return encodingFound; + } + + public static Encoding DetectBOMBytes(byte[] BOMBytes) + { + if (BOMBytes.Length < 2) + return null; + + if (BOMBytes[0] == 0xff + && BOMBytes[1] == 0xfe + && (BOMBytes.Length < 4 + || BOMBytes[2] != 0 + || BOMBytes[3] != 0 + ) + ) + return Encoding.Unicode; + + if (BOMBytes[0] == 0xfe + && BOMBytes[1] == 0xff + ) + return Encoding.BigEndianUnicode; + + if (BOMBytes.Length < 3) + return null; + + if (BOMBytes[0] == 0xef && BOMBytes[1] == 0xbb && BOMBytes[2] == 0xbf) + return Encoding.UTF8; + + if (BOMBytes[0] == 0x2b && BOMBytes[1] == 0x2f && BOMBytes[2] == 0x76) + return Encoding.UTF8; + + if (BOMBytes.Length < 4) + return null; + + if (BOMBytes[0] == 0xff && BOMBytes[1] == 0xfe && BOMBytes[2] == 0 && BOMBytes[3] == 0) + return Encoding.UTF32; + + if (BOMBytes[0] == 0 && BOMBytes[1] == 0 && BOMBytes[2] == 0xfe && BOMBytes[3] == 0xff) + return Encoding.GetEncoding(12001); + + return null; + } + + public static Encoding DetectUnicodeInByteSampleByHeuristics(byte[] SampleBytes) + { + long oddBinaryNullsInSample = 0; + long evenBinaryNullsInSample = 0; + long suspiciousUTF8SequenceCount = 0; + long suspiciousUTF8BytesTotal = 0; + long likelyUSASCIIBytesInSample = 0; + + //Cycle through, keeping count of binary null positions, possible UTF-8 + // sequences from upper ranges of Windows-1252, and probable US-ASCII + // character counts. + + long currentPos = 0; + int skipUTF8Bytes = 0; + + while (currentPos < SampleBytes.Length) + { + //binary null distribution + if (SampleBytes[currentPos] == 0) + { + if (currentPos % 2 == 0) + evenBinaryNullsInSample++; + else + oddBinaryNullsInSample++; + } + + //likely US-ASCII characters + if (IsCommonUSASCIIByte(SampleBytes[currentPos])) + likelyUSASCIIBytesInSample++; + + //suspicious sequences (look like UTF-8) + if (skipUTF8Bytes == 0) + { + int lengthFound = DetectSuspiciousUTF8SequenceLength(SampleBytes, currentPos); + + if (lengthFound > 0) + { + suspiciousUTF8SequenceCount++; + suspiciousUTF8BytesTotal += lengthFound; + skipUTF8Bytes = lengthFound - 1; + } + } + else + { + skipUTF8Bytes--; + } + + currentPos++; + } + + //1: UTF-16 LE - in english / european environments, this is usually characterized by a + // high proportion of odd binary nulls (starting at 0), with (as this is text) a low + // proportion of even binary nulls. + // The thresholds here used (less than 20% nulls where you expect non-nulls, and more than + // 60% nulls where you do expect nulls) are completely arbitrary. + + if (((evenBinaryNullsInSample * 2.0) / SampleBytes.Length) < 0.2 + && ((oddBinaryNullsInSample * 2.0) / SampleBytes.Length) > 0.6 + ) + return Encoding.Unicode; + + + //2: UTF-16 BE - in english / european environments, this is usually characterized by a + // high proportion of even binary nulls (starting at 0), with (as this is text) a low + // proportion of odd binary nulls. + // The thresholds here used (less than 20% nulls where you expect non-nulls, and more than + // 60% nulls where you do expect nulls) are completely arbitrary. + + if (((oddBinaryNullsInSample * 2.0) / SampleBytes.Length) < 0.2 + && ((evenBinaryNullsInSample * 2.0) / SampleBytes.Length) > 0.6 + ) + return Encoding.BigEndianUnicode; + + + //3: UTF-8 - Martin Dürst outlines a method for detecting whether something CAN be UTF-8 content + // using regexp, in his w3c.org unicode FAQ entry: + // http://www.w3.org/International/questions/qa-forms-utf-8 + // adapted here for C#. + string potentiallyMangledString = Encoding.ASCII.GetString(SampleBytes); + Regex UTF8Validator = new Regex(@"\A(" + + @"[\x09\x0A\x0D\x20-\x7E]" + + @"|[\xC2-\xDF][\x80-\xBF]" + + @"|\xE0[\xA0-\xBF][\x80-\xBF]" + + @"|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}" + + @"|\xED[\x80-\x9F][\x80-\xBF]" + + @"|\xF0[\x90-\xBF][\x80-\xBF]{2}" + + @"|[\xF1-\xF3][\x80-\xBF]{3}" + + @"|\xF4[\x80-\x8F][\x80-\xBF]{2}" + + @")*\z"); + if (UTF8Validator.IsMatch(potentiallyMangledString)) + { + //Unfortunately, just the fact that it CAN be UTF-8 doesn't tell you much about probabilities. + //If all the characters are in the 0-127 range, no harm done, most western charsets are same as UTF-8 in these ranges. + //If some of the characters were in the upper range (western accented characters), however, they would likely be mangled to 2-byte by the UTF-8 encoding process. + // So, we need to play stats. + + // The "Random" likelihood of any pair of randomly generated characters being one + // of these "suspicious" character sequences is: + // 128 / (256 * 256) = 0.2%. + // + // In western text data, that is SIGNIFICANTLY reduced - most text data stays in the <127 + // character range, so we assume that more than 1 in 500,000 of these character + // sequences indicates UTF-8. The number 500,000 is completely arbitrary - so sue me. + // + // We can only assume these character sequences will be rare if we ALSO assume that this + // IS in fact western text - in which case the bulk of the UTF-8 encoded data (that is + // not already suspicious sequences) should be plain US-ASCII bytes. This, I + // arbitrarily decided, should be 80% (a random distribution, eg binary data, would yield + // approx 40%, so the chances of hitting this threshold by accident in random data are + // VERY low). + + if ((suspiciousUTF8SequenceCount * 500000.0 / SampleBytes.Length >= 1) //suspicious sequences + && ( + //all suspicious, so cannot evaluate proportion of US-Ascii + SampleBytes.Length - suspiciousUTF8BytesTotal == 0 + || + likelyUSASCIIBytesInSample * 1.0 / (SampleBytes.Length - suspiciousUTF8BytesTotal) >= 0.8 + ) + ) + return Encoding.UTF8; + } + + return null; + } + + private static bool IsCommonUSASCIIByte(byte testByte) + { + if (testByte == 0x0A //lf + || testByte == 0x0D //cr + || testByte == 0x09 //tab + || (testByte >= 0x20 && testByte <= 0x2F) //common punctuation + || (testByte >= 0x30 && testByte <= 0x39) //digits + || (testByte >= 0x3A && testByte <= 0x40) //common punctuation + || (testByte >= 0x41 && testByte <= 0x5A) //capital letters + || (testByte >= 0x5B && testByte <= 0x60) //common punctuation + || (testByte >= 0x61 && testByte <= 0x7A) //lowercase letters + || (testByte >= 0x7B && testByte <= 0x7E) //common punctuation + ) + return true; + else + return false; + } + + private static int DetectSuspiciousUTF8SequenceLength(byte[] SampleBytes, long currentPos) + { + int lengthFound = 0; + + if (SampleBytes.Length >= currentPos + 1 + && SampleBytes[currentPos] == 0xC2 + ) + { + if (SampleBytes[currentPos + 1] == 0x81 + || SampleBytes[currentPos + 1] == 0x8D + || SampleBytes[currentPos + 1] == 0x8F + ) + lengthFound = 2; + else if (SampleBytes[currentPos + 1] == 0x90 + || SampleBytes[currentPos + 1] == 0x9D + ) + lengthFound = 2; + else if (SampleBytes[currentPos + 1] >= 0xA0 + && SampleBytes[currentPos + 1] <= 0xBF + ) + lengthFound = 2; + } + else if (SampleBytes.Length >= currentPos + 1 + && SampleBytes[currentPos] == 0xC3 + ) + { + if (SampleBytes[currentPos + 1] >= 0x80 + && SampleBytes[currentPos + 1] <= 0xBF + ) + lengthFound = 2; + } + else if (SampleBytes.Length >= currentPos + 1 + && SampleBytes[currentPos] == 0xC5 + ) + { + if (SampleBytes[currentPos + 1] == 0x92 + || SampleBytes[currentPos + 1] == 0x93 + ) + lengthFound = 2; + else if (SampleBytes[currentPos + 1] == 0xA0 + || SampleBytes[currentPos + 1] == 0xA1 + ) + lengthFound = 2; + else if (SampleBytes[currentPos + 1] == 0xB8 + || SampleBytes[currentPos + 1] == 0xBD + || SampleBytes[currentPos + 1] == 0xBE + ) + lengthFound = 2; + } + else if (SampleBytes.Length >= currentPos + 1 + && SampleBytes[currentPos] == 0xC6 + ) + { + if (SampleBytes[currentPos + 1] == 0x92) + lengthFound = 2; + } + else if (SampleBytes.Length >= currentPos + 1 + && SampleBytes[currentPos] == 0xCB + ) + { + if (SampleBytes[currentPos + 1] == 0x86 + || SampleBytes[currentPos + 1] == 0x9C + ) + lengthFound = 2; + } + else if (SampleBytes.Length >= currentPos + 2 + && SampleBytes[currentPos] == 0xE2 + ) + { + if (SampleBytes[currentPos + 1] == 0x80) + { + if (SampleBytes[currentPos + 2] == 0x93 + || SampleBytes[currentPos + 2] == 0x94 + ) + lengthFound = 3; + if (SampleBytes[currentPos + 2] == 0x98 + || SampleBytes[currentPos + 2] == 0x99 + || SampleBytes[currentPos + 2] == 0x9A + ) + lengthFound = 3; + if (SampleBytes[currentPos + 2] == 0x9C + || SampleBytes[currentPos + 2] == 0x9D + || SampleBytes[currentPos + 2] == 0x9E + ) + lengthFound = 3; + if (SampleBytes[currentPos + 2] == 0xA0 + || SampleBytes[currentPos + 2] == 0xA1 + || SampleBytes[currentPos + 2] == 0xA2 + ) + lengthFound = 3; + if (SampleBytes[currentPos + 2] == 0xA6) + lengthFound = 3; + if (SampleBytes[currentPos + 2] == 0xB0) + lengthFound = 3; + if (SampleBytes[currentPos + 2] == 0xB9 + || SampleBytes[currentPos + 2] == 0xBA + ) + lengthFound = 3; + } + else if (SampleBytes[currentPos + 1] == 0x82 + && SampleBytes[currentPos + 2] == 0xAC + ) + lengthFound = 3; + else if (SampleBytes[currentPos + 1] == 0x84 + && SampleBytes[currentPos + 2] == 0xA2 + ) + lengthFound = 3; + } + + return lengthFound; + } + } +} diff --git a/src/TTGamesExplorerRebirthUI/FastColoredTextBox/ExportToHTML.cs b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/ExportToHTML.cs new file mode 100644 index 0000000..c2e07b9 --- /dev/null +++ b/src/TTGamesExplorerRebirthUI/FastColoredTextBox/ExportToHTML.cs @@ -0,0 +1,222 @@ +using System.Text; +using System.Drawing; +using System.Collections.Generic; + +namespace FastColoredTextBoxNS +{ + /// + /// Exports colored text as HTML + /// + /// At this time only TextStyle renderer is supported. Other styles is not exported. + public class ExportToHTML + { + public string LineNumbersCSS = ""; + + /// + /// Use nbsp; instead space + /// + public bool UseNbsp { get; set; } + /// + /// Use nbsp; instead space in beginning of line + /// + public bool UseForwardNbsp { get; set; } + /// + /// Use original font + /// + public bool UseOriginalFont { get; set; } + /// + /// Use style tag instead style attribute + /// + public bool UseStyleTag { get; set; } + /// + /// Use 'br' tag instead of '\n' + /// + public bool UseBr { get; set; } + /// + /// Includes line numbers + /// + public bool IncludeLineNumbers { get; set; } + + FastColoredTextBox tb; + + public ExportToHTML() + { + UseNbsp = true; + UseOriginalFont = true; + UseStyleTag = true; + UseBr = true; + } + + public string GetHtml(FastColoredTextBox tb) + { + this.tb = tb; + Range sel = new Range(tb); + sel.SelectAll(); + return GetHtml(sel); + } + + public string GetHtml(Range r) + { + this.tb = r.tb; + Dictionary styles = new Dictionary(); + StringBuilder sb = new StringBuilder(); + StringBuilder tempSB = new StringBuilder(); + StyleIndex currentStyleId = StyleIndex.None; + r.Normalize(); + int currentLine = r.Start.iLine; + styles[currentStyleId] = null; + // + if (UseOriginalFont) + sb.AppendFormat("", + r.tb.Font.Name, r.tb.Font.SizeInPoints, r.tb.CharHeight); + + // + if (IncludeLineNumbers) + tempSB.AppendFormat("{0} ", currentLine + 1); + // + bool hasNonSpace = false; + foreach (Place p in r) + { + Char c = r.tb[p.iLine][p.iChar]; + if (c.style != currentStyleId) + { + Flush(sb, tempSB, currentStyleId); + currentStyleId = c.style; + styles[currentStyleId] = null; + } + + if (p.iLine != currentLine) + { + for (int i = currentLine; i < p.iLine; i++) + { + tempSB.Append(UseBr ? "
" : "\r\n"); + if (IncludeLineNumbers) + tempSB.AppendFormat("{0} ", i + 2); + } + currentLine = p.iLine; + hasNonSpace = false; + } + switch (c.c) + { + case ' ': + if ((hasNonSpace || !UseForwardNbsp) && !UseNbsp) + goto default; + + tempSB.Append(" "); + break; + case '<': + tempSB.Append("<"); + break; + case '>': + tempSB.Append(">"); + break; + case '&': + tempSB.Append("&"); + break; + default: + hasNonSpace = true; + tempSB.Append(c.c); + break; + } + } + Flush(sb, tempSB, currentStyleId); + + if (UseOriginalFont) + sb.Append("
"); + + //build styles + if (UseStyleTag) + { + tempSB.Length = 0; + tempSB.Append(""); + + sb.Insert(0, tempSB.ToString()); + } + + if (IncludeLineNumbers) + sb.Insert(0, LineNumbersCSS); + + return sb.ToString(); + } + + private string GetCss(StyleIndex styleIndex) + { + List