Make ImageConverter polymorphic and revert back to texcov on Windows (#2281)

* Make ImageConverter polymorphic and revert back to texcov on Windows

* Add files I forgot to add, make CHANGELOG.md additions

* Don't run texconv tests on Linux/OSX
This commit is contained in:
Timothy Baldridge 2023-01-21 13:36:12 -06:00 committed by GitHub
parent 31ec53aa29
commit 5b77574b5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 353 additions and 65 deletions

View File

@ -4,7 +4,7 @@
* Add support for Cubemaps in BA2 files, if you have problems with BA2 recompression, be sure to delete your `GlobalVFSCache3.sqlite` from your AppData before the next compile * Add support for Cubemaps in BA2 files, if you have problems with BA2 recompression, be sure to delete your `GlobalVFSCache3.sqlite` from your AppData before the next compile
* Fixed slides not being shown during installation for lists compile with the 3.0 compiler * Fixed slides not being shown during installation for lists compile with the 3.0 compiler
* Set the "While loading slide" debug message to be `Trace` level, set the default minimum log level to `Information` * Set the "While loading slide" debug message to be `Trace` level, set the default minimum log level to `Information`
* Switched back to using TexConv for texture converting on Windows, should greatly improve compatability of texture conversion (on windows systems)
#### Version - 3.0.5.0 - 12/22/2022 #### Version - 3.0.5.0 - 12/22/2022
* Add support for https://www.nexusmods.com/site hosted mods. * Add support for https://www.nexusmods.com/site hosted mods.

View File

@ -25,4 +25,5 @@ public static class Ext
public static Extension ModlistMetadataExtension = new(".modlist_metadata"); public static Extension ModlistMetadataExtension = new(".modlist_metadata");
public static Extension Txt = new(".txt"); public static Extension Txt = new(".txt");
public static Extension Webp = new(".webp"); public static Extension Webp = new(".webp");
public static Extension Png = new(".png");
} }

View File

@ -28,10 +28,13 @@ public class CompilerSanityTests : IAsyncLifetime
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private Mod _mod; private Mod _mod;
private ModList? _modlist; private ModList? _modlist;
private readonly IImageLoader _imageLoader;
public CompilerSanityTests(ILogger<CompilerSanityTests> logger, IServiceProvider serviceProvider, public CompilerSanityTests(ILogger<CompilerSanityTests> logger, IServiceProvider serviceProvider,
FileExtractor.FileExtractor fileExtractor, FileExtractor.FileExtractor fileExtractor,
TemporaryFileManager manager, ParallelOptions parallelOptions) TemporaryFileManager manager,
ParallelOptions parallelOptions,
IImageLoader imageLoader)
{ {
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
@ -40,6 +43,7 @@ public class CompilerSanityTests : IAsyncLifetime
_fileExtractor = fileExtractor; _fileExtractor = fileExtractor;
_manager = manager; _manager = manager;
_parallelOptions = parallelOptions; _parallelOptions = parallelOptions;
_imageLoader = imageLoader;
} }
@ -195,12 +199,12 @@ public class CompilerSanityTests : IAsyncLifetime
foreach (var file in _mod.FullPath.EnumerateFiles()) foreach (var file in _mod.FullPath.EnumerateFiles())
{ {
var oldState = await ImageLoader.Load(file); var oldState = await _imageLoader.Load(file);
Assert.NotEqual(DXGI_FORMAT.UNKNOWN, oldState.Format); Assert.NotEqual(DXGI_FORMAT.UNKNOWN, oldState.Format);
_logger.LogInformation("Recompressing {file}", file.FileName); _logger.LogInformation("Recompressing {file}", file.FileName);
await ImageLoader.Recompress(file, 512, 512, DXGI_FORMAT.BC7_UNORM, file, CancellationToken.None); await _imageLoader.Recompress(file, 512, 512, DXGI_FORMAT.BC7_UNORM, file, CancellationToken.None);
var state = await ImageLoader.Load(file); var state = await _imageLoader.Load(file);
Assert.Equal(DXGI_FORMAT.BC7_UNORM, state.Format); Assert.Equal(DXGI_FORMAT.BC7_UNORM, state.Format);
} }

View File

@ -17,6 +17,7 @@ using Wabbajack.DTOs.Directives;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.JsonConverters;
using Wabbajack.FileExtractor.ExtractedFiles; using Wabbajack.FileExtractor.ExtractedFiles;
using Wabbajack.Hashing.PHash;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Installer; using Wabbajack.Installer;
using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Networking.WabbajackClientApi;
@ -64,7 +65,8 @@ public abstract class ACompiler
TemporaryFileManager manager, CompilerSettings settings, TemporaryFileManager manager, CompilerSettings settings,
ParallelOptions parallelOptions, DownloadDispatcher dispatcher, Client wjClient, IGameLocator locator, ParallelOptions parallelOptions, DownloadDispatcher dispatcher, Client wjClient, IGameLocator locator,
DTOSerializer dtos, IResource<ACompiler> compilerLimiter, DTOSerializer dtos, IResource<ACompiler> compilerLimiter,
IBinaryPatchCache patchCache) IBinaryPatchCache patchCache,
IImageLoader imageLoader)
{ {
CompilerLimiter = compilerLimiter; CompilerLimiter = compilerLimiter;
_logger = logger; _logger = logger;
@ -83,9 +85,12 @@ public abstract class ACompiler
_patchOptions = new ConcurrentDictionary<PatchedFromArchive, VirtualFile[]>(); _patchOptions = new ConcurrentDictionary<PatchedFromArchive, VirtualFile[]>();
_sourceFileLinks = new ConcurrentDictionary<Directive, RawSourceFile>(); _sourceFileLinks = new ConcurrentDictionary<Directive, RawSourceFile>();
_patchCache = patchCache; _patchCache = patchCache;
ImageLoader = imageLoader;
_updateStopWatch = new Stopwatch(); _updateStopWatch = new Stopwatch();
} }
public IImageLoader ImageLoader { get; }
protected long MaxSteps { get; set; } protected long MaxSteps { get; set; }
public CompilerSettings Settings public CompilerSettings Settings

View File

@ -34,7 +34,7 @@ public class MatchSimilarTextures : ACompilationStep
_compiler._logger.LogInformation("Looking for texture match for {source}", source.File.FullPath); _compiler._logger.LogInformation("Looking for texture match for {source}", source.File.FullPath);
(float Similarity, VirtualFile File) found = _byName[source.Path.FileNameWithoutExtension] (float Similarity, VirtualFile File) found = _byName[source.Path.FileNameWithoutExtension]
.Select(f => ( .Select(f => (
ImageLoader.ComputeDifference(f.ImageState!.PerceptualHash, source.File.ImageState.PerceptualHash), IImageLoader.ComputeDifference(f.ImageState!.PerceptualHash, source.File.ImageState.PerceptualHash),
f)) f))
.Select(f => { return f; }) .Select(f => { return f; })
.OrderByDescending(f => f.Item1) .OrderByDescending(f => f.Item1)
@ -58,7 +58,7 @@ public class MatchSimilarTextures : ACompilationStep
from mainMatch in _byName[mainFile.FullPath.FileName.FileNameWithoutExtension] from mainMatch in _byName[mainFile.FullPath.FileName.FileNameWithoutExtension]
where mainMatch.ImageState != null where mainMatch.ImageState != null
where mainFile.ImageState != null where mainFile.ImageState != null
let similarity = ImageLoader.ComputeDifference(mainFile.ImageState!.PerceptualHash, let similarity = IImageLoader.ComputeDifference(mainFile.ImageState!.PerceptualHash,
mainMatch.ImageState!.PerceptualHash) mainMatch.ImageState!.PerceptualHash)
where similarity >= PerceptualTolerance where similarity >= PerceptualTolerance
orderby similarity descending orderby similarity descending

View File

@ -14,6 +14,7 @@ using Wabbajack.Downloaders.GameFile;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.Directives; using Wabbajack.DTOs.Directives;
using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Hashing.PHash;
using Wabbajack.Installer; using Wabbajack.Installer;
using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths; using Wabbajack.Paths;
@ -30,9 +31,10 @@ public class MO2Compiler : ACompiler
TemporaryFileManager manager, CompilerSettings settings, ParallelOptions parallelOptions, TemporaryFileManager manager, CompilerSettings settings, ParallelOptions parallelOptions,
DownloadDispatcher dispatcher, DownloadDispatcher dispatcher,
Client wjClient, IGameLocator locator, DTOSerializer dtos, IResource<ACompiler> compilerLimiter, Client wjClient, IGameLocator locator, DTOSerializer dtos, IResource<ACompiler> compilerLimiter,
IBinaryPatchCache patchCache) : IBinaryPatchCache patchCache,
IImageLoader imageLoader) :
base(logger, extractor, hashCache, vfs, manager, settings, parallelOptions, dispatcher, wjClient, locator, dtos, base(logger, extractor, hashCache, vfs, manager, settings, parallelOptions, dispatcher, wjClient, locator, dtos,
compilerLimiter, patchCache) compilerLimiter, patchCache, imageLoader)
{ {
MaxSteps = 14; MaxSteps = 14;
} }
@ -51,7 +53,8 @@ public class MO2Compiler : ACompiler
provider.GetRequiredService<IGameLocator>(), provider.GetRequiredService<IGameLocator>(),
provider.GetRequiredService<DTOSerializer>(), provider.GetRequiredService<DTOSerializer>(),
provider.GetRequiredService<IResource<ACompiler>>(), provider.GetRequiredService<IResource<ACompiler>>(),
provider.GetRequiredService<IBinaryPatchCache>()); provider.GetRequiredService<IBinaryPatchCache>(),
provider.GetRequiredService<IImageLoader>());
} }
public CompilerSettings Mo2Settings => (CompilerSettings) Settings; public CompilerSettings Mo2Settings => (CompilerSettings) Settings;

View File

@ -1,4 +1,6 @@
using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -10,8 +12,31 @@ using Xunit;
namespace Wabbajack.Hashing.PHash.Test; namespace Wabbajack.Hashing.PHash.Test;
public class FileLoadingTests public class FileLoadingTests : IAsyncDisposable
{ {
private readonly IImageLoader[] _imageLoaders;
private readonly TemporaryFileManager _tmp;
public FileLoadingTests()
{
_tmp = new TemporaryFileManager();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
_imageLoaders = new IImageLoader[]
{
new CrossPlatformImageLoader(),
new TexConvImageLoader(_tmp)
};
}
else
{
_imageLoaders = new IImageLoader[]
{
new CrossPlatformImageLoader(),
};
}
}
[Theory] [Theory]
[InlineData("test-dxt5.dds", 1.0f)] [InlineData("test-dxt5.dds", 1.0f)]
[InlineData("test-dxt5-recompressed.dds", 1f)] [InlineData("test-dxt5-recompressed.dds", 1f)]
@ -19,40 +44,47 @@ public class FileLoadingTests
[InlineData("test-dxt5-small-bc7-vflip.dds", 0.189f)] [InlineData("test-dxt5-small-bc7-vflip.dds", 0.189f)]
public async Task LoadAllFiles(string file, float difference) public async Task LoadAllFiles(string file, float difference)
{ {
var baseState = foreach (var imageLoader in _imageLoaders)
await ImageLoader.Load("TestData/test-dxt5.dds".ToRelativePath().RelativeTo(KnownFolders.EntryPoint)); {
var state = await ImageLoader.Load("TestData".ToRelativePath().Combine(file) var baseState =
.RelativeTo(KnownFolders.EntryPoint)); await imageLoader.Load("TestData/test-dxt5.dds".ToRelativePath().RelativeTo(KnownFolders.EntryPoint));
var state = await imageLoader.Load("TestData".ToRelativePath().Combine(file)
.RelativeTo(KnownFolders.EntryPoint));
Assert.NotEqual(DXGI_FORMAT.UNKNOWN, baseState.Format); Assert.NotEqual(DXGI_FORMAT.UNKNOWN, baseState.Format);
Assert.Equal(difference, Assert.Equal(difference,
ImagePhash.GetCrossCorrelation( ImagePhash.GetCrossCorrelation(
new Digest {Coefficients = baseState.PerceptualHash.Data}, new Digest { Coefficients = baseState.PerceptualHash.Data },
new Digest {Coefficients = state.PerceptualHash.Data}), new Digest { Coefficients = state.PerceptualHash.Data }),
1.0); 1.0);
}
} }
[Fact] [Fact]
public async Task CanConvertCubeMaps() public async Task CanConvertCubeMaps()
{ {
// File used here via re-upload permissions found on the mod's Nexus page: foreach (var imageLoader in _imageLoaders)
// https://www.nexusmods.com/fallout4/mods/43458?tab=description {
// Used for testing purposes only // File used here via re-upload permissions found on the mod's Nexus page:
var path = "TestData/WindowDisabled_CGPlayerHouseCube.dds".ToRelativePath().RelativeTo(KnownFolders.EntryPoint); // https://www.nexusmods.com/fallout4/mods/43458?tab=description
// Used for testing purposes only
var path = "TestData/WindowDisabled_CGPlayerHouseCube.dds".ToRelativePath().RelativeTo(KnownFolders.EntryPoint);
var baseState = await ImageLoader.Load(path); var baseState = await imageLoader.Load(path);
baseState.Height.Should().Be(128); baseState.Height.Should().Be(128);
baseState.Width.Should().Be(128); baseState.Width.Should().Be(128);
//baseState.Frames.Should().Be(6); //baseState.Frames.Should().Be(6);
using var ms = new MemoryStream();
await using var ins = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
await ImageLoader.Recompress(ins, 128, 128, DXGI_FORMAT.BC1_UNORM, ms, CancellationToken.None, leaveOpen:true);
ms.Length.Should().Be(ins.Length);
using var ms = new MemoryStream();
await using var ins = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
await imageLoader.Recompress(ins, 128, 128, DXGI_FORMAT.BC1_UNORM, ms, CancellationToken.None, leaveOpen:true);
ms.Length.Should().Be(ins.Length);
}
} }
public async ValueTask DisposeAsync()
{
await _tmp.DisposeAsync();
}
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BCnEncoder.Decoder; using BCnEncoder.Decoder;
@ -13,21 +14,22 @@ using Shipwreck.Phash.Imaging;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using Wabbajack.Common;
using Wabbajack.DTOs.Texture; using Wabbajack.DTOs.Texture;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
namespace Wabbajack.Hashing.PHash; namespace Wabbajack.Hashing.PHash;
public class ImageLoader public class CrossPlatformImageLoader : IImageLoader
{ {
public static async ValueTask<ImageState> Load(AbsolutePath path) public async ValueTask<ImageState> Load(AbsolutePath path)
{ {
await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read); await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
return await Load(fs); return await Load(fs);
} }
public static async ValueTask<ImageState> Load(Stream stream) public async ValueTask<ImageState> Load(Stream stream)
{ {
var decoder = new BcDecoder(); var decoder = new BcDecoder();
var ddsFile = DdsFile.Load(stream); var ddsFile = DdsFile.Load(stream);
@ -57,8 +59,8 @@ public class ImageLoader
new Digest {Coefficients = a.Data}, new Digest {Coefficients = a.Data},
new Digest {Coefficients = b.Data}); new Digest {Coefficients = b.Data});
} }
public static async Task Recompress(AbsolutePath input, int width, int height, DXGI_FORMAT format, public async Task Recompress(AbsolutePath input, int width, int height, DXGI_FORMAT format,
AbsolutePath output, AbsolutePath output,
CancellationToken token) CancellationToken token)
{ {
@ -67,24 +69,23 @@ public class ImageLoader
await Recompress(new MemoryStream(inData), width, height, format, outStream, token); await Recompress(new MemoryStream(inData), width, height, format, outStream, token);
} }
public static async Task Recompress(Stream input, int width, int height, DXGI_FORMAT format, Stream output, public async Task Recompress(Stream input, int width, int height, DXGI_FORMAT format, Stream output,
CancellationToken token, bool leaveOpen = false) CancellationToken token, bool leaveOpen = false)
{ {
var decoder = new BcDecoder(); var decoder = new BcDecoder();
var ddsFile = DdsFile.Load(input); var ddsFile = DdsFile.Load(input);
if (!leaveOpen) await input.DisposeAsync(); if (!leaveOpen) await input.DisposeAsync();
var faces = new List<Image<Rgba32>>(); var faces = new List<Image<Rgba32>>();
var origFormat = ddsFile.dx10Header.dxgiFormat == DxgiFormat.DxgiFormatUnknown var origFormat = ddsFile.dx10Header.dxgiFormat == DxgiFormat.DxgiFormatUnknown
? ddsFile.header.ddsPixelFormat.DxgiFormat ? ddsFile.header.ddsPixelFormat.DxgiFormat
: ddsFile.dx10Header.dxgiFormat; : ddsFile.dx10Header.dxgiFormat;
foreach (var face in ddsFile.Faces) foreach (var face in ddsFile.Faces)
{ {
var data = await decoder.DecodeRawToImageRgba32Async(face.MipMaps[0].Data,
var data = await decoder.DecodeRawToImageRgba32Async(face.MipMaps[0].Data,
(int)face.Width, (int)face.Height, ToCompressionFormat((DXGI_FORMAT)origFormat), token); (int)face.Width, (int)face.Height, ToCompressionFormat((DXGI_FORMAT)origFormat), token);
data.Mutate(x => x.Resize(width, height, KnownResamplers.Welch)); data.Mutate(x => x.Resize(width, height, KnownResamplers.Welch));
@ -101,7 +102,7 @@ public class ImageLoader
FileFormat = OutputFileFormat.Dds FileFormat = OutputFileFormat.Dds
} }
}; };
switch (faces.Count) switch (faces.Count)
{ {
case 1: case 1:
@ -114,11 +115,11 @@ public class ImageLoader
default: default:
throw new NotImplementedException($"Can't encode dds with {faces.Count} faces"); throw new NotImplementedException($"Can't encode dds with {faces.Count} faces");
} }
if (!leaveOpen) if (!leaveOpen)
await output.DisposeAsync(); await output.DisposeAsync();
} }
public static CompressionFormat ToCompressionFormat(DXGI_FORMAT dx) public static CompressionFormat ToCompressionFormat(DXGI_FORMAT dx)
{ {
return dx switch return dx switch

View File

@ -0,0 +1,27 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Shipwreck.Phash;
using Wabbajack.DTOs.Texture;
using Wabbajack.Paths;
namespace Wabbajack.Hashing.PHash;
public interface IImageLoader
{
public ValueTask<ImageState> Load(AbsolutePath path);
public ValueTask<ImageState> Load(Stream stream);
public static float ComputeDifference(DTOs.Texture.PHash a, DTOs.Texture.PHash b)
{
return ImagePhash.GetCrossCorrelation(
new Digest {Coefficients = a.Data},
new Digest {Coefficients = b.Data});
}
public Task Recompress(AbsolutePath input, int width, int height, DXGI_FORMAT format,
AbsolutePath output,
CancellationToken token);
public Task Recompress(Stream input, int width, int height, DXGI_FORMAT format, Stream output,
CancellationToken token, bool leaveOpen = false);
}

View File

@ -0,0 +1,160 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Shipwreck.Phash;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Wabbajack.Common;
using Wabbajack.Common.FileSignatures;
using Wabbajack.DTOs.Texture;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.Hashing.PHash;
public class TexConvImageLoader : IImageLoader
{
private readonly SignatureChecker _sigs;
private readonly TemporaryFileManager _tempManager;
public TexConvImageLoader(TemporaryFileManager manager)
{
_tempManager = manager;
_sigs = new SignatureChecker(FileType.DDS, FileType.PNG, FileType.JPG, FileType.BMP);
}
public async ValueTask<ImageState> Load(AbsolutePath path)
{
return await GetState(path);
}
public async ValueTask<ImageState> Load(Stream stream)
{
var ext = await DetermineType(stream);
var temp = _tempManager.CreateFile(ext);
await using var fs = temp.Path.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await stream.CopyToAsync(fs);
fs.Close();
return await GetState(temp.Path);
}
private async Task<Extension> DetermineType(Stream stream)
{
var sig = await _sigs.MatchesAsync(stream);
var ext = new Extension(".tga");
if (sig != null)
ext = new Extension("." + Enum.GetName(sig.Value));
stream.Position = 0;
return ext;
}
public async Task Recompress(AbsolutePath input, int width, int height, DXGI_FORMAT format, AbsolutePath output,
CancellationToken token)
{
var outFolder = _tempManager.CreateFolder();
var outFile = input.FileName.RelativeTo(outFolder.Path);
await ConvertImage(input, outFolder.Path, width, height, format, input.Extension);
await outFile.MoveToAsync(output, token: token, overwrite:true);
}
public async Task Recompress(Stream input, int width, int height, DXGI_FORMAT format, Stream output, CancellationToken token,
bool leaveOpen = false)
{
var type = await DetermineType(input);
await using var toFolder = _tempManager.CreateFolder();
await using var fromFile = _tempManager.CreateFile(type);
await input.CopyToAsync(fromFile.Path, token);
var toFile = fromFile.Path.FileName.RelativeTo(toFolder);
await ConvertImage(fromFile.Path, toFolder.Path, width, height, format, type);
await using var fs = toFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
await fs.CopyToAsync(output, token);
}
public async Task ConvertImage(AbsolutePath from, AbsolutePath toFolder, int w, int h, DXGI_FORMAT format, Extension fileFormat)
{
// User isn't renaming the file, so we don't have to create a temporary folder
var ph = new ProcessHelper
{
Path = @"Tools\texconv.exe".ToRelativePath().RelativeTo(KnownFolders.EntryPoint),
Arguments = new object[] {from, "-ft", fileFormat.ToString()[1..], "-f", format, "-o", toFolder, "-w", w, "-h", h, "-if", "CUBIC", "-singleproc"},
ThrowOnNonZeroExitCode = true,
LogError = true
};
await ph.Start();
}
public async Task ConvertImage(Stream from, ImageState state, Extension ext, AbsolutePath to)
{
await using var tmpFile = _tempManager.CreateFolder();
var inFile = to.FileName.RelativeTo(tmpFile.Path);
await inFile.WriteAllAsync(from, CancellationToken.None);
await ConvertImage(inFile, to.Parent, state.Width, state.Height, state.Format, ext);
}
// Internals
public async Task<ImageState> GetState(AbsolutePath path)
{
try
{
var ph = new ProcessHelper
{
Path = @"Tools\texdiag.exe".ToRelativePath().RelativeTo(KnownFolders.EntryPoint),
Arguments = new object[] {"info", path, "-nologo"},
ThrowOnNonZeroExitCode = true,
LogError = true
};
var lines = new ConcurrentStack<string>();
using var _ = ph.Output.Where(p => p.Type == ProcessHelper.StreamType.Output)
.Select(p => p.Line)
.Where(p => p.Contains(" = "))
.Subscribe(l => lines.Push(l));
await ph.Start();
var data = lines.Select(l =>
{
var split = l.Split(" = ");
return (split[0].Trim(), split[1].Trim());
}).ToDictionary(p => p.Item1, p => p.Item2);
return new ImageState
{
Width = int.Parse(data["width"]),
Height = int.Parse(data["height"]),
Format = Enum.Parse<DXGI_FORMAT>(data["format"]),
PerceptualHash = await GetPHash(path)
};
}
catch (Exception ex)
{
throw;
}
}
public async Task<DTOs.Texture.PHash> GetPHash(AbsolutePath path)
{
if (!path.FileExists())
throw new FileNotFoundException($"Can't hash non-existent file {path}");
await using var tmp = _tempManager.CreateFolder();
await ConvertImage(path, tmp.Path, 512, 512, DXGI_FORMAT.R8G8B8A8_UNORM, Ext.Png);
using var img = await Image.LoadAsync(path.FileName.RelativeTo(tmp.Path).ReplaceExtension(Ext.Png).ToString());
img.Mutate(x => x.Resize(512, 512, KnownResamplers.Welch).Grayscale(GrayscaleMode.Bt601));
return new DTOs.Texture.PHash(ImagePhash.ComputeDigest(new CrossPlatformImageLoader.ImageBitmap((Image<Rgba32>)img)).Coefficients);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -13,9 +13,23 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" />
<ProjectReference Include="..\Wabbajack.DTOs\Wabbajack.DTOs.csproj" /> <ProjectReference Include="..\Wabbajack.DTOs\Wabbajack.DTOs.csproj" />
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" /> <ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />
<ProjectReference Include="..\Wabbajack.Paths\Wabbajack.Paths.csproj" /> <ProjectReference Include="..\Wabbajack.Paths\Wabbajack.Paths.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Tools" />
</ItemGroup>
<ItemGroup>
<None Update="Tools\texconv.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Tools\texdiag.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@ -75,7 +75,8 @@ public abstract class AInstaller<T>
DownloadDispatcher downloadDispatcher, DownloadDispatcher downloadDispatcher,
ParallelOptions parallelOptions, ParallelOptions parallelOptions,
IResource<IInstaller> limiter, IResource<IInstaller> limiter,
Client wjClient) Client wjClient,
IImageLoader imageLoader)
{ {
_limiter = limiter; _limiter = limiter;
_manager = new TemporaryFileManager(config.Install.Combine("__temp__")); _manager = new TemporaryFileManager(config.Install.Combine("__temp__"));
@ -90,8 +91,11 @@ public abstract class AInstaller<T>
_parallelOptions = parallelOptions; _parallelOptions = parallelOptions;
_gameLocator = gameLocator; _gameLocator = gameLocator;
_wjClient = wjClient; _wjClient = wjClient;
ImageLoader = imageLoader;
} }
public IImageLoader ImageLoader { get; }
protected long MaxSteps { get; set; } protected long MaxSteps { get; set; }
public Dictionary<Hash, AbsolutePath> HashedArchives { get; set; } = new(); public Dictionary<Hash, AbsolutePath> HashedArchives { get; set; } = new();

View File

@ -23,6 +23,7 @@ using Wabbajack.DTOs.BSA.FileStates;
using Wabbajack.DTOs.Directives; using Wabbajack.DTOs.Directives;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Hashing.PHash;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Installer.Utilities; using Wabbajack.Installer.Utilities;
using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Networking.WabbajackClientApi;
@ -40,9 +41,9 @@ public class StandardInstaller : AInstaller<StandardInstaller>
InstallerConfiguration config, InstallerConfiguration config,
IGameLocator gameLocator, FileExtractor.FileExtractor extractor, IGameLocator gameLocator, FileExtractor.FileExtractor extractor,
DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache, DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache,
DownloadDispatcher downloadDispatcher, ParallelOptions parallelOptions, IResource<IInstaller> limiter, Client wjClient) : DownloadDispatcher downloadDispatcher, ParallelOptions parallelOptions, IResource<IInstaller> limiter, Client wjClient, IImageLoader imageLoader) :
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher, base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
parallelOptions, limiter, wjClient) parallelOptions, limiter, wjClient, imageLoader)
{ {
MaxSteps = 14; MaxSteps = 14;
} }
@ -59,7 +60,8 @@ public class StandardInstaller : AInstaller<StandardInstaller>
provider.GetRequiredService<DownloadDispatcher>(), provider.GetRequiredService<DownloadDispatcher>(),
provider.GetRequiredService<ParallelOptions>(), provider.GetRequiredService<ParallelOptions>(),
provider.GetRequiredService<IResource<IInstaller>>(), provider.GetRequiredService<IResource<IInstaller>>(),
provider.GetRequiredService<Client>()); provider.GetRequiredService<Client>(),
provider.GetRequiredService<IImageLoader>());
} }
public override async Task<bool> Begin(CancellationToken token) public override async Task<bool> Begin(CancellationToken token)

View File

@ -252,6 +252,18 @@ public static class AbsolutePathExtensions
return file.ToString(); return file.ToString();
} }
public static async Task CopyToAsync(this Stream from, AbsolutePath path, CancellationToken token = default)
{
await using var to = path.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await from.CopyToAsync(to, token);
}
public static async Task CopyToAsync(this AbsolutePath from, Stream to, CancellationToken token = default)
{
await using var fromStream = from.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
await fromStream.CopyToAsync(to, token);
}
#region Directories #region Directories
public static void CreateDirectory(this AbsolutePath path) public static void CreateDirectory(this AbsolutePath path)

View File

@ -85,7 +85,12 @@ public struct AbsolutePath : IPath, IComparable<AbsolutePath>, IEquatable<Absolu
} }
} }
public AbsolutePath ReplaceExtension(Extension newExtension) /// <summary>
/// Returns a new path that is this path with the extension changed.
/// </summary>
/// <param name="newExtension"></param>
/// <returns></returns>
public readonly AbsolutePath ReplaceExtension(Extension newExtension)
{ {
var paths = new string[Parts.Length]; var paths = new string[Parts.Length];
Array.Copy(Parts, paths, paths.Length); Array.Copy(Parts, paths, paths.Length);
@ -94,7 +99,7 @@ public struct AbsolutePath : IPath, IComparable<AbsolutePath>, IEquatable<Absolu
paths[^1] = newName; paths[^1] = newName;
return new AbsolutePath(paths, PathFormat); return new AbsolutePath(paths, PathFormat);
} }
public static explicit operator AbsolutePath(string input) public static explicit operator AbsolutePath(string input)
{ {
return Parse(input); return Parse(input);

View File

@ -48,12 +48,10 @@ public struct RelativePath : IPath, IEquatable<RelativePath>, IComparable<Relati
internal static string ReplaceExtension(string oldName, Extension newExtension) internal static string ReplaceExtension(string oldName, Extension newExtension)
{ {
var oldExtLength = oldName.LastIndexOf(".", StringComparison.CurrentCultureIgnoreCase); var oldExtLength = oldName.LastIndexOf(".", StringComparison.CurrentCultureIgnoreCase);
if (oldExtLength < 0) if (oldExtLength <= 0)
oldExtLength = 0; return oldName + newExtension;
else
oldExtLength++; var newName = oldName[..oldExtLength] + newExtension;
var newName = oldName[..^oldExtLength] + newExtension;
return newName; return newName;
} }

View File

@ -15,6 +15,7 @@ using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.JsonConverters;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
using Wabbajack.Hashing.PHash;
using Wabbajack.Installer; using Wabbajack.Installer;
using Wabbajack.Networking.BethesdaNet; using Wabbajack.Networking.BethesdaNet;
using Wabbajack.Networking.Discord; using Wabbajack.Networking.Discord;
@ -180,6 +181,12 @@ public static class ServiceExtensions
service.AddAllSingleton<IGameLocator, StubbedGameLocator>(); service.AddAllSingleton<IGameLocator, StubbedGameLocator>();
else else
service.AddAllSingleton<IGameLocator, GameLocator>(); service.AddAllSingleton<IGameLocator, GameLocator>();
// ImageLoader
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
service.AddSingleton<IImageLoader, TexConvImageLoader>();
else
service.AddSingleton<IImageLoader, CrossPlatformImageLoader>();
// Installer/Compiler Configuration // Installer/Compiler Configuration
service.AddScoped<InstallerConfiguration>(); service.AddScoped<InstallerConfiguration>();

View File

@ -1,7 +1,9 @@
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.Hashing.PHash;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
using Wabbajack.VFS.Interfaces; using Wabbajack.VFS.Interfaces;
@ -28,6 +30,13 @@ public class Startup
.AddAllSingleton<IResource, IResource<FileHashCache>, Resource<FileHashCache>>( .AddAllSingleton<IResource, IResource<FileHashCache>, Resource<FileHashCache>>(
s => s =>
new ("File Hash Cache", 2)); new ("File Hash Cache", 2));
// ImageLoader
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
service.AddSingleton<IImageLoader, TexConvImageLoader>();
else
service.AddSingleton<IImageLoader, CrossPlatformImageLoader>();
// Keep this fixed at 2 so that we can detect deadlocks in the VFS parallelOptions // Keep this fixed at 2 so that we can detect deadlocks in the VFS parallelOptions
service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = 2}); service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = 2});

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.FileExtractor.ExtractedFiles; using Wabbajack.FileExtractor.ExtractedFiles;
using Wabbajack.Hashing.PHash;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
@ -32,7 +33,8 @@ public class Context
public Context(ILogger<Context> logger, ParallelOptions parallelOptions, TemporaryFileManager manager, public Context(ILogger<Context> logger, ParallelOptions parallelOptions, TemporaryFileManager manager,
IVfsCache vfsCache, IVfsCache vfsCache,
FileHashCache hashCache, IResource<Context> limiter, IResource<FileHashCache> hashLimiter, FileExtractor.FileExtractor extractor) FileHashCache hashCache, IResource<Context> limiter, IResource<FileHashCache> hashLimiter,
FileExtractor.FileExtractor extractor, IImageLoader imageLoader)
{ {
Limiter = limiter; Limiter = limiter;
HashLimiter = hashLimiter; HashLimiter = hashLimiter;
@ -42,16 +44,18 @@ public class Context
VfsCache = vfsCache; VfsCache = vfsCache;
HashCache = hashCache; HashCache = hashCache;
_parallelOptions = parallelOptions; _parallelOptions = parallelOptions;
ImageLoader = imageLoader;
} }
public Context WithTemporaryFileManager(TemporaryFileManager manager) public Context WithTemporaryFileManager(TemporaryFileManager manager)
{ {
return new Context(Logger, _parallelOptions, manager, VfsCache, HashCache, Limiter, HashLimiter, return new Context(Logger, _parallelOptions, manager, VfsCache, HashCache, Limiter, HashLimiter,
Extractor.WithTemporaryFileManager(manager)); Extractor.WithTemporaryFileManager(manager), ImageLoader);
} }
public IndexRoot Index { get; private set; } = IndexRoot.Empty; public IndexRoot Index { get; private set; } = IndexRoot.Empty;
public IImageLoader ImageLoader { get; }
public async Task<IndexRoot> AddRoot(AbsolutePath root, CancellationToken token) public async Task<IndexRoot> AddRoot(AbsolutePath root, CancellationToken token)
{ {

View File

@ -204,7 +204,7 @@ public class VirtualFile
if (TextureExtensions.Contains(relPath.FileName.Extension) && await DDSSig.MatchesAsync(stream) != null) if (TextureExtensions.Contains(relPath.FileName.Extension) && await DDSSig.MatchesAsync(stream) != null)
try try
{ {
self.ImageState = await ImageLoader.Load(stream); self.ImageState = await context.ImageLoader.Load(stream);
if (job != null) if (job != null)
{ {
job.Size += self.Size; job.Size += self.Size;