* Add some support for cubemaps in BA2

* add read support for cubemaps in BA2 routines

* Fix slide support during compilation

* Set default logging level to Information instead of Trace

* update CHANGELOG.md
This commit is contained in:
Timothy Baldridge 2022-12-28 10:21:58 -06:00 committed by GitHub
parent e5ae5acb08
commit 4bfce0e418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 968 additions and 754 deletions

View File

@ -1,5 +1,10 @@
### Changelog ### Changelog
#### Version - 3.0.6.0 - ??
* 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
* Set the "While loading slide" debug message to be `Trace` level, set the default minimum log level to `Information`
#### 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.
* Fix Website Links * Fix Website Links

View File

@ -145,7 +145,7 @@ namespace Wabbajack
config.AddRuleForAllLevels(uiTarget); config.AddRuleForAllLevels(uiTarget);
loggingBuilder.ClearProviders(); loggingBuilder.ClearProviders();
loggingBuilder.SetMinimumLevel(LogLevel.Trace); loggingBuilder.SetMinimumLevel(LogLevel.Information);
loggingBuilder.AddNLog(config); loggingBuilder.AddNLog(config);
} }

View File

@ -505,7 +505,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "While loading slide"); _logger.LogTrace(ex, "While loading slide");
} }
} }

View File

@ -1,4 +1,5 @@
using System.IO; using System;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Wabbajack.DTOs.Texture; using Wabbajack.DTOs.Texture;
@ -66,6 +67,19 @@ public enum DXT10_RESOURCE_DIMENSION
DIMENSION_TEXTURE3D = 4 DIMENSION_TEXTURE3D = 4
} }
[Flags]
public enum DDSCAPS2 : uint
{
CUBEMAP = 0x200,
CUBEMAP_POSITIVEX = 0x400,
CUBEMAP_NEGATIVEX = 0x800,
CUBEMAP_POSITIVEY = 0x1000,
CUBEMAP_NEGATIVEY = 0x2000,
CUBEMAP_POSITIVEZ = 0x4000,
CUBEMAP_NEGATIVEZ = 0x8000,
CUBEMAP_ALLFACES = 0xFC00
}
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DDS_HEADER public struct DDS_HEADER
{ {

View File

@ -18,19 +18,21 @@ namespace Wabbajack.Compression.BSA.FO4Archive;
public class DX10Entry : IBA2FileEntry public class DX10Entry : IBA2FileEntry
{ {
private readonly Reader _bsa; private readonly Reader _bsa;
internal ushort _chunkHdrLen; private ushort _chunkHdrLen;
internal List<TextureChunk> _chunks; private List<TextureChunk> _chunks;
internal uint _dirHash; private uint _dirHash;
internal string _extension; private string _extension;
internal byte _format; private byte _format;
internal ushort _height; private ushort _height;
internal int _index; private int _index;
internal uint _nameHash; private uint _nameHash;
internal byte _numChunks; private byte _numChunks;
internal byte _numMips; private byte _numMips;
internal ushort _unk16; private ushort _unk16;
internal byte _unk8; private byte _unk8;
internal ushort _width; private ushort _width;
private readonly byte _isCubemap;
private readonly byte _tileMode;
public DX10Entry(Reader ba2Reader, int idx) public DX10Entry(Reader ba2Reader, int idx)
{ {
@ -47,7 +49,8 @@ public class DX10Entry : IBA2FileEntry
_width = _rdr.ReadUInt16(); _width = _rdr.ReadUInt16();
_numMips = _rdr.ReadByte(); _numMips = _rdr.ReadByte();
_format = _rdr.ReadByte(); _format = _rdr.ReadByte();
_unk16 = _rdr.ReadUInt16(); _isCubemap = _rdr.ReadByte();
_tileMode = _rdr.ReadByte();
_index = idx; _index = idx;
_chunks = Enumerable.Range(0, _numChunks) _chunks = Enumerable.Range(0, _numChunks)
@ -74,7 +77,8 @@ public class DX10Entry : IBA2FileEntry
Width = _width, Width = _width,
NumMips = _numMips, NumMips = _numMips,
PixelFormat = _format, PixelFormat = _format,
Unk16 = _unk16, IsCubeMap = _isCubemap,
TileMode = _tileMode,
Index = _index, Index = _index,
Chunks = _chunks.Select(ch => new BA2Chunk Chunks = _chunks.Select(ch => new BA2Chunk
{ {
@ -139,6 +143,12 @@ public class DX10Entry : IBA2FileEntry
ddsHeader.PixelFormat.dwSize = ddsHeader.PixelFormat.GetSize(); ddsHeader.PixelFormat.dwSize = ddsHeader.PixelFormat.GetSize();
ddsHeader.dwDepth = 1; ddsHeader.dwDepth = 1;
ddsHeader.dwSurfaceFlags = DDS.DDS_SURFACE_FLAGS_TEXTURE | DDS.DDS_SURFACE_FLAGS_MIPMAP; ddsHeader.dwSurfaceFlags = DDS.DDS_SURFACE_FLAGS_TEXTURE | DDS.DDS_SURFACE_FLAGS_MIPMAP;
ddsHeader.dwCubemapFlags = _isCubemap == 1 ? (uint)(DDSCAPS2.CUBEMAP
| DDSCAPS2.CUBEMAP_NEGATIVEX | DDSCAPS2.CUBEMAP_POSITIVEX
| DDSCAPS2.CUBEMAP_NEGATIVEY | DDSCAPS2.CUBEMAP_POSITIVEY
| DDSCAPS2.CUBEMAP_NEGATIVEZ | DDSCAPS2.CUBEMAP_POSITIVEZ
| DDSCAPS2.CUBEMAP_ALLFACES) : 0u;
switch ((DXGI_FORMAT) _format) switch ((DXGI_FORMAT) _format)
{ {

View File

@ -31,7 +31,8 @@ public class DX10FileEntryBuilder : IFileBuilder
bw.Write(_state.Width); bw.Write(_state.Width);
bw.Write(_state.NumMips); bw.Write(_state.NumMips);
bw.Write(_state.PixelFormat); bw.Write(_state.PixelFormat);
bw.Write(_state.Unk16); bw.Write((byte)_state.IsCubeMap);
bw.Write((byte)_state.TileMode);
foreach (var chunk in _chunks) foreach (var chunk in _chunks)
chunk.WriteHeader(bw); chunk.WriteHeader(bw);

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,6 @@ public class BA2DX10File : AFile
{ {
public BA2Chunk[] Chunks { get; set; } public BA2Chunk[] Chunks { get; set; }
public ushort Unk16 { get; set; }
public byte PixelFormat { get; set; } public byte PixelFormat { get; set; }
public byte NumMips { get; set; } public byte NumMips { get; set; }
@ -27,4 +25,6 @@ public class BA2DX10File : AFile
public string Extension { get; set; } public string Extension { get; set; }
public uint NameHash { get; set; } public uint NameHash { get; set; }
public byte IsCubeMap { get; set; }
public byte TileMode { get; set; }
} }

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Downloaders.VerificationCache;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
using Xunit; using Xunit;
@ -10,32 +12,41 @@ namespace Wabbajack.Downloaders.Dispatcher.Test;
public class VerificationCacheTests public class VerificationCacheTests
{ {
private readonly ILogger<VerificationCache.VerificationCache> _logger; private readonly ILogger<VerificationCache.VerificationCache> _logger;
private readonly DTOSerializer _dtos;
public VerificationCacheTests(ILogger<VerificationCache.VerificationCache> logger) public VerificationCacheTests(ILogger<VerificationCache.VerificationCache> logger, DTOSerializer dtos)
{ {
_logger = logger; _logger = logger;
_dtos = dtos;
} }
[Fact] [Fact]
public async Task BasicCacheTests() public async Task BasicCacheTests()
{ {
using var cache = new VerificationCache.VerificationCache(_logger, KnownFolders.EntryPoint.Combine(Guid.NewGuid().ToString()), TimeSpan.FromSeconds(1)); using var cacheBase = new VerificationCache.VerificationCache(_logger,
KnownFolders.EntryPoint.Combine(Guid.NewGuid().ToString()),
TimeSpan.FromSeconds(1),
_dtos);
var cache = (IVerificationCache)cacheBase;
var goodState = new DTOs.DownloadStates.Http { Url = new Uri($"https://some.com/{Guid.NewGuid()}/path") }; var goodState = new DTOs.DownloadStates.Http { Url = new Uri($"https://some.com/{Guid.NewGuid()}/path") };
var badState = new DTOs.DownloadStates.Http { Url = new Uri($"https://some.com/{Guid.NewGuid()}/path") }; var badState = new DTOs.DownloadStates.Http { Url = new Uri($"https://some.com/{Guid.NewGuid()}/path") };
Assert.True(await cache.Get(goodState) == null); Assert.True((await cache.Get(goodState)).IsValid == null);
await cache.Put(goodState, true); await cache.Put(goodState, true);
Assert.True(await cache.Get(goodState)); var result = await cache.Get(goodState);
Assert.True(result.IsValid);
Assert.IsType<DTOs.DownloadStates.Http>(result.State);
await Task.Delay(TimeSpan.FromSeconds(2)); await Task.Delay(TimeSpan.FromSeconds(2));
Assert.False(await cache.Get(goodState)); Assert.False((await cache.Get(goodState)).IsValid);
await cache.Put(badState, true); await cache.Put(badState, true);
Assert.True(await cache.Get(badState)); Assert.True((await cache.Get(badState)).IsValid);
await cache.Put(badState, false); await cache.Put(badState, false);
Assert.Null(await cache.Get(badState)); Assert.Null((await cache.Get(badState)).IsValid);
} }
} }

View File

@ -112,8 +112,12 @@ public class DownloadDispatcher
{ {
try try
{ {
if (await _verificationCache.Get(a.State) == true) var (valid, newState) = await _verificationCache.Get(a.State);
if (valid == true)
{
a.State = newState;
return true; return true;
}
a = await MaybeProxy(a, token); a = await MaybeProxy(a, token);
var downloader = Downloader(a); var downloader = Downloader(a);

View File

@ -5,6 +5,6 @@ namespace Wabbajack.Downloaders.VerificationCache;
public interface IVerificationCache public interface IVerificationCache
{ {
Task<bool?> Get(IDownloadState archive); Task<(bool? IsValid, IDownloadState State)> Get(IDownloadState archive);
Task Put(IDownloadState archive, bool valid); Task Put(IDownloadState archive, bool valid);
} }

View File

@ -1,7 +1,9 @@
using System.Data.SQLite; using System.Data.SQLite;
using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
@ -15,12 +17,14 @@ public class VerificationCache : IVerificationCache, IDisposable
private readonly SQLiteConnection _conn; private readonly SQLiteConnection _conn;
private readonly TimeSpan _expiry; private readonly TimeSpan _expiry;
private readonly ILogger<VerificationCache> _logger; private readonly ILogger<VerificationCache> _logger;
private readonly DTOSerializer _dtos;
public VerificationCache(ILogger<VerificationCache> logger, AbsolutePath location, TimeSpan expiry) public VerificationCache(ILogger<VerificationCache> logger, AbsolutePath location, TimeSpan expiry, DTOSerializer dtos)
{ {
_logger = logger; _logger = logger;
_location = location; _location = location;
_expiry = expiry; _expiry = expiry;
_dtos = dtos;
if (!_location.Parent.DirectoryExists()) if (!_location.Parent.DirectoryExists())
_location.Parent.CreateDirectory(); _location.Parent.CreateDirectory();
@ -34,17 +38,18 @@ public class VerificationCache : IVerificationCache, IDisposable
using var cmd = new SQLiteCommand(_conn); using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS VerficationCache ( cmd.CommandText = @"CREATE TABLE IF NOT EXISTS VerficationCache (
PKS TEXT PRIMARY KEY, PKS TEXT PRIMARY KEY,
LastModified BIGINT) LastModified BIGINT,
State TEXT)
WITHOUT ROWID"; WITHOUT ROWID";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
public async Task<bool?> Get(IDownloadState archive) public async Task<(bool?, IDownloadState?)> Get(IDownloadState archive)
{ {
var key = archive.PrimaryKeyString; var key = archive.PrimaryKeyString;
await using var cmd = new SQLiteCommand(_conn); await using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = "SELECT LastModified FROM VerficationCache WHERE PKS = @pks"; cmd.CommandText = "SELECT LastModified, State FROM VerficationCache WHERE PKS = @pks";
cmd.Parameters.AddWithValue("@pks", key); cmd.Parameters.AddWithValue("@pks", key);
await cmd.PrepareAsync(); await cmd.PrepareAsync();
@ -52,10 +57,12 @@ public class VerificationCache : IVerificationCache, IDisposable
while (await reader.ReadAsync()) while (await reader.ReadAsync())
{ {
var ts = DateTime.FromFileTimeUtc(reader.GetInt64(0)); var ts = DateTime.FromFileTimeUtc(reader.GetInt64(0));
return DateTime.UtcNow - ts <= _expiry; var state = JsonSerializer.Deserialize<IDownloadState>(reader.GetString(1), _dtos.Options);
return (DateTime.UtcNow - ts <= _expiry, state);
} }
return null; return (null, null);
} }
public async Task Put(IDownloadState state, bool valid) public async Task Put(IDownloadState state, bool valid)
@ -64,10 +71,11 @@ public class VerificationCache : IVerificationCache, IDisposable
if (valid) if (valid)
{ {
await using var cmd = new SQLiteCommand(_conn); await using var cmd = new SQLiteCommand(_conn);
cmd.CommandText = @"INSERT INTO VerficationCache (PKS, LastModified) VALUES (@pks, @lastModified) cmd.CommandText = @"INSERT INTO VerficationCache (PKS, LastModified, State) VALUES (@pks, @lastModified, @state)
ON CONFLICT(PKS) DO UPDATE SET LastModified = @lastModified"; ON CONFLICT(PKS) DO UPDATE SET LastModified = @lastModified, State = @state";
cmd.Parameters.AddWithValue("@pks", key); cmd.Parameters.AddWithValue("@pks", key);
cmd.Parameters.AddWithValue("@lastModified", DateTime.UtcNow.ToFileTimeUtc()); cmd.Parameters.AddWithValue("@lastModified", DateTime.UtcNow.ToFileTimeUtc());
cmd.Parameters.AddWithValue("@state", JsonSerializer.Serialize(state, _dtos.Options));
await cmd.PrepareAsync(); await cmd.PrepareAsync();
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();

View File

@ -1,4 +1,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions;
using Shipwreck.Phash; using Shipwreck.Phash;
using Wabbajack.DTOs.Texture; using Wabbajack.DTOs.Texture;
using Wabbajack.Paths; using Wabbajack.Paths;
@ -29,4 +32,27 @@ public class FileLoadingTests
new Digest {Coefficients = state.PerceptualHash.Data}), new Digest {Coefficients = state.PerceptualHash.Data}),
1.0); 1.0);
} }
[Fact]
public async Task CanConvertCubeMaps()
{
// File used here via re-upload permissions found on the mod's Nexus page:
// 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);
baseState.Height.Should().Be(128);
baseState.Width.Should().Be(128);
//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);
}
} }

View File

@ -11,6 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0-preview-20221003-04" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0-preview-20221003-04" />
<PackageReference Include="Shipwreck.Phash" Version="0.5.0" /> <PackageReference Include="Shipwreck.Phash" Version="0.5.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
@ -38,6 +39,9 @@
<None Update="TestData\test-dxt5-small-bc7-vflip.dds"> <None Update="TestData\test-dxt5-small-bc7-vflip.dds">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="TestData\WindowDisabled_CGPlayerHouseCube.dds">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -74,9 +75,21 @@ public class ImageLoader
if (!leaveOpen) await input.DisposeAsync(); if (!leaveOpen) await input.DisposeAsync();
var data = await decoder.DecodeToImageRgba32Async(ddsFile, token); var faces = new List<Image<Rgba32>>();
var origFormat = ddsFile.dx10Header.dxgiFormat == DxgiFormat.DxgiFormatUnknown
? ddsFile.header.ddsPixelFormat.DxgiFormat
: ddsFile.dx10Header.dxgiFormat;
foreach (var face in ddsFile.Faces)
{
var data = await decoder.DecodeRawToImageRgba32Async(face.MipMaps[0].Data,
(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));
faces.Add(data);
}
var encoder = new BcEncoder var encoder = new BcEncoder
{ {
@ -88,8 +101,19 @@ public class ImageLoader
FileFormat = OutputFileFormat.Dds FileFormat = OutputFileFormat.Dds
} }
}; };
var file = await encoder.EncodeToDdsAsync(data, token);
file.Write(output); switch (faces.Count)
{
case 1:
(await encoder.EncodeToDdsAsync(faces[0], token)).Write(output);
break;
case 6:
(await encoder.EncodeCubeMapToDdsAsync(faces[0], faces[1], faces[2], faces[3], faces[4], faces[5], token))
.Write(output);
break;
default:
throw new NotImplementedException($"Can't encode dds with {faces.Count} faces");
}
if (!leaveOpen) if (!leaveOpen)
await output.DisposeAsync(); await output.DisposeAsync();

View File

@ -12,6 +12,7 @@ public static class KnownFolders
{ {
get get
{ {
return AppDomain.CurrentDomain.BaseDirectory.ToAbsolutePath();
var result = Process.GetCurrentProcess().MainModule?.FileName?.ToAbsolutePath() ?? default; var result = Process.GetCurrentProcess().MainModule?.FileName?.ToAbsolutePath() ?? default;
if (result != default && if (result != default &&

View File

@ -13,6 +13,7 @@ using Wabbajack.Downloaders.GameFile;
using Wabbajack.Downloaders.VerificationCache; using Wabbajack.Downloaders.VerificationCache;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
using Wabbajack.Installer; using Wabbajack.Installer;
using Wabbajack.Networking.BethesdaNet; using Wabbajack.Networking.BethesdaNet;
@ -73,9 +74,19 @@ public static class ServiceExtensions
: new BinaryPatchCache(s.GetRequiredService<ILogger<BinaryPatchCache>>(),KnownFolders.WabbajackAppLocal.Combine("PatchCache"))); : new BinaryPatchCache(s.GetRequiredService<ILogger<BinaryPatchCache>>(),KnownFolders.WabbajackAppLocal.Combine("PatchCache")));
service.AddSingleton<IVerificationCache>(s => options.UseLocalCache service.AddSingleton<IVerificationCache>(s =>
? new VerificationCache(s.GetRequiredService<ILogger<VerificationCache>>(), s.GetService<TemporaryFileManager>()!.CreateFile().Path, TimeSpan.FromDays(1)) {
: new VerificationCache(s.GetRequiredService<ILogger<VerificationCache>>(),KnownFolders.WabbajackAppLocal.Combine("VerificationCache.sqlite"), TimeSpan.FromDays(1))); var dtos = s.GetRequiredService<DTOSerializer>();
return options.UseLocalCache
? new VerificationCache(s.GetRequiredService<ILogger<VerificationCache>>(),
s.GetService<TemporaryFileManager>()!.CreateFile().Path,
TimeSpan.FromDays(1),
dtos)
: new VerificationCache(s.GetRequiredService<ILogger<VerificationCache>>(),
KnownFolders.WabbajackAppLocal.Combine("VerificationCacheV2.sqlite"),
TimeSpan.FromDays(1),
dtos);
});
service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount}); service.AddSingleton(new ParallelOptions {MaxDegreeOfParallelism = Environment.ProcessorCount});