Compare commits

...

21 Commits

Author SHA1 Message Date
defnotken
84a3293f6b eighty three
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m15s
2026-01-20 10:33:21 -06:00
defnotken
28db3d6fd2 Merge branch '2.0.3' into dev 2026-01-20 10:32:54 -06:00
d83ca98008 Merge pull request 'updated ptrguard to use system_info for minimum calculations' (#144) from clr-fix-attempt into 2.0.3
Reviewed-on: #144
2026-01-20 16:09:57 +00:00
e6df37bcca Merge pull request 'slight-tweaks' (#143) from slight-tweaks into 2.0.3
Reviewed-on: #143
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2026-01-20 15:49:04 +00:00
60d144b881 adjust log level 2026-01-21 00:47:55 +09:00
cake
995e11371a updated ptrguard to use system_info for minimum calculations 2026-01-20 16:42:31 +01:00
5089dbd6c8 move texture and model processing out of download thread and fix some normalization for weights 2026-01-21 00:17:04 +09:00
defnotken
abc324bf4f bumpers
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m23s
2026-01-19 19:48:39 -06:00
defnotken
eee0e072bd Merge branch '2.0.3' into dev 2026-01-19 19:48:28 -06:00
cake
d8335eb04f Removal of log 2026-01-20 02:44:50 +01:00
cake
994335c6b0 Moved to trace log 2026-01-20 02:40:30 +01:00
defnotken
172288c755 bumpers
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 1m57s
2026-01-19 18:31:36 -06:00
defnotken
1c17be53d0 Merge branch '2.0.3' into dev 2026-01-19 18:30:42 -06:00
68b4863f52 Merge pull request 'Added CLR fix for gameobject handler' (#142) from clr-fix-attempt into 2.0.3
Reviewed-on: #142
2026-01-20 00:02:58 +00:00
cake
22fe9901a4 Fixed some issues. 2026-01-20 00:25:26 +01:00
cake
cff866dcc2 Added ptrguard to be used whenever ptrs are being used. 2026-01-20 00:24:42 +01:00
defnotken
e8f598e695 bumpity bump
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m7s
2026-01-19 12:36:24 -06:00
defnotken
861a337029 Merge branch '2.0.3' into dev
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
2026-01-19 12:35:29 -06:00
06f89955d3 Merge pull request 'clr-fix-attempt' (#141) from clr-fix-attempt into 2.0.3
Reviewed-on: #141
2026-01-19 18:33:55 +00:00
cake
367af2c3d0 Remove blankspace 2026-01-19 19:26:00 +01:00
cake
19a238c808 Removal of unsafe and check if PTR is correctly aligning, checking in virtual query instead of memory 2026-01-19 19:24:44 +01:00
14 changed files with 444 additions and 96 deletions

View File

@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
{
token.ThrowIfCancellationRequested();
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
logger.LogDebug("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
await convertTask.ConfigureAwait(false);

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>2.0.2.79</Version>
<Version>2.0.2.83</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -1,6 +1,5 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using LightlessSync.API.Data.Enum;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
@@ -14,6 +13,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Factories;
@@ -119,46 +119,30 @@ public class PlayerDataFactory
return null;
}
private static readonly int _drawObjectOffset =
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
{
if (playerPointer == IntPtr.Zero)
return true;
if (!IsPointerValid(playerPointer))
return true;
var character = (Character*)playerPointer;
if (character == null)
return true;
var gameObject = &character->GameObject;
if (gameObject == null)
return true;
if (!IsPointerValid((IntPtr)gameObject))
return true;
return gameObject->DrawObject == null;
}
private static bool IsPointerValid(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
return false;
try
=> await _dalamudUtil.RunOnFrameworkThread(() =>
{
_ = Marshal.ReadByte(ptr);
return true;
}
catch
{
return false;
}
}
nint basePtr = playerPointer;
if (!PtrGuard.LooksLikePtr(basePtr))
return true;
nint drawObjAddr = basePtr + _drawObjectOffset;
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
return true;
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
return true;
if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj))
return true;
return drawObj == 0;
}).ConfigureAwait(false);
private static bool IsCacheFresh(CacheEntry entry)
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;

View File

@@ -2,11 +2,12 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.PlayerData.Handlers;
@@ -177,18 +178,41 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
var prevDrawObj = DrawObjectAddress;
string? nameString = null;
Address = _getAddress();
var nextAddr = _getAddress();
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
{
nextAddr = IntPtr.Zero;
}
if (nextAddr != IntPtr.Zero &&
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
{
nextAddr = IntPtr.Zero;
}
Address = nextAddr;
if (Address != IntPtr.Zero)
{
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
var draw = (nint)gameObject->DrawObject;
if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject)))
draw = 0;
DrawObjectAddress = draw;
EntityId = gameObject->EntityId;
var chara = (Character*)Address;
nameString = chara->GameObject.NameString;
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
Name = nameString;
if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character)))
{
var chara = (Character*)Address;
nameString = chara->GameObject.NameString;
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
Name = nameString;
}
}
else
{
@@ -196,22 +220,27 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
EntityId = uint.MaxValue;
}
CurrentDrawCondition = IsBeingDrawnUnsafe();
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
? IsBeingDrawnUnsafe()
: DrawCondition.DrawObjectZero;
if (_haltProcessing || !allowPublish) return;
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
bool addrDiff = Address != prevAddr;
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
{
var chara = (Character*)Address;
var drawObj = (DrawObject*)DrawObjectAddress;
var objType = drawObj->Object.GetObjectType();
var isHuman = objType == ObjectType.CharacterBase
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
nameString ??= ((Character*)Address)->GameObject.NameString;
nameString ??= chara->GameObject.NameString;
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
if (nameChange) Name = nameString;
@@ -219,32 +248,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
if (isHuman)
{
var classJob = chara->CharacterData.ClassJob;
if (classJob != _classJob)
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
{
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
_classJob = classJob;
Mediator.Publish(new ClassJobChangedMessage(this));
var classJob = chara->CharacterData.ClassJob;
if (classJob != _classJob)
{
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
_classJob = classJob;
Mediator.Publish(new ClassJobChangedMessage(this));
}
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
}
else
{
isHuman = false;
}
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
if (equipDiff)
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
}
else
if (!isHuman)
{
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
if (equipDiff)
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
}
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
if (equipDiff && !_isOwnedObject)
{
Logger.LogTrace("[{this}] Changed", this);
return;
@@ -252,11 +285,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
bool customizeDiff = false;
if (isHuman)
if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
{
var gender = ((Human*)drawObj)->Customize.Sex;
var raceId = ((Human*)drawObj)->Customize.Race;
var tribeId = ((Human*)drawObj)->Customize.Tribe;
var human = (Human*)drawObj;
var gender = human->Customize.Sex;
var raceId = human->Customize.Race;
var tribeId = human->Customize.Tribe;
if (_isOwnedObject && ObjectKind == ObjectKind.Player
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
@@ -267,15 +302,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
TribeId = tribeId;
}
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
if (customizeDiff)
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data);
}
else
{
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
if (customizeDiff)
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
}
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
@@ -289,12 +320,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
CurrentDrawCondition = DrawCondition.DrawObjectZero;
Logger.LogTrace("[{this}] Changed", this);
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
{
Mediator.Publish(new ClearCacheForObjectMessage(this));
}
}
}
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
{
bool hasChanges = false;
@@ -330,7 +360,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
{
if ((nint)weapon == nint.Zero) return false;
var p = (nint)weapon;
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
return false;
bool hasChanges = false;
hasChanges |= weapon->ModelSetId != MainHandData[0];
MainHandData[0] = weapon->ModelSetId;
@@ -343,7 +376,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
{
if ((nint)weapon == nint.Zero) return false;
var p = (nint)weapon;
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
return false;
bool hasChanges = false;
hasChanges |= weapon->ModelSetId != OffHandData[0];
OffHandData[0] = weapon->ModelSetId;

View File

@@ -125,6 +125,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<FileTransferOrchestrator>();
services.AddSingleton<LightlessPlugin>();
services.AddSingleton<LightlessProfileManager>();
services.AddSingleton<TextureProcessingQueue>();
services.AddSingleton<ModelProcessingQueue>();
services.AddSingleton<TextureCompressionService>();
services.AddSingleton<TextureDownscaleService>();
services.AddSingleton<ModelDecimationService>();

View File

@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace LightlessSync.Services;
public sealed class AssetProcessingQueue : IDisposable
{
private readonly BlockingCollection<WorkItem> _queue = new();
private readonly Thread _worker;
private readonly ILogger _logger;
private bool _disposed;
public AssetProcessingQueue(ILogger logger, string name)
{
_logger = logger;
_worker = new Thread(Run)
{
IsBackground = true,
Name = string.IsNullOrWhiteSpace(name) ? "LightlessSync.AssetProcessing" : name
};
_worker.Start();
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
{
if (work is null)
{
throw new ArgumentNullException(nameof(work));
}
var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
if (token.IsCancellationRequested)
{
completion.TrySetCanceled(token);
return completion.Task;
}
if (_queue.IsAddingCompleted || _disposed)
{
completion.TrySetException(new ObjectDisposedException(nameof(AssetProcessingQueue)));
return completion.Task;
}
_queue.Add(new WorkItem(work, token, completion));
return completion.Task;
}
private void Run()
{
foreach (var item in _queue.GetConsumingEnumerable())
{
if (item.Token.IsCancellationRequested)
{
item.Completion.TrySetCanceled(item.Token);
continue;
}
try
{
item.Work(item.Token).GetAwaiter().GetResult();
item.Completion.TrySetResult(null);
}
catch (OperationCanceledException ex)
{
var token = ex.CancellationToken.IsCancellationRequested ? ex.CancellationToken : item.Token;
item.Completion.TrySetCanceled(token);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Asset processing job failed.");
item.Completion.TrySetException(ex);
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_queue.CompleteAdding();
_worker.Join(TimeSpan.FromSeconds(2));
_queue.Dispose();
}
private readonly record struct WorkItem(
Func<CancellationToken, Task> Work,
CancellationToken Token,
TaskCompletionSource<object?> Completion);
}

View File

@@ -2104,6 +2104,16 @@ internal static class MdlDecimator
}
}
if (boneWeights != null
&& blendWeightEncoding == BlendWeightEncoding.Default
&& format.BlendWeightsElement is { } blendWeightsElement
&& (MdlFile.VertexType)blendWeightsElement.Type == MdlFile.VertexType.UShort4
&& ShouldTreatWeightsAsByteNormalized(boneWeights))
{
RescaleUShortAsByteWeights(boneWeights);
blendWeightEncoding = BlendWeightEncoding.UShortAsByte;
}
decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
return true;
}
@@ -3413,6 +3423,44 @@ internal static class MdlDecimator
return ToUShortNormalized(normalized);
}
private static bool ShouldTreatWeightsAsByteNormalized(BoneWeight[] weights)
{
const float maxByteUnorm = byte.MaxValue / (float)ushort.MaxValue;
var maxWeight = 0f;
for (var i = 0; i < weights.Length; i++)
{
var weight = weights[i];
maxWeight = Math.Max(maxWeight, weight.weight0);
maxWeight = Math.Max(maxWeight, weight.weight1);
maxWeight = Math.Max(maxWeight, weight.weight2);
maxWeight = Math.Max(maxWeight, weight.weight3);
if (maxWeight > maxByteUnorm)
{
return false;
}
}
return maxWeight > 0f;
}
private static void RescaleUShortAsByteWeights(BoneWeight[] weights)
{
var scale = ushort.MaxValue / (float)byte.MaxValue;
for (var i = 0; i < weights.Length; i++)
{
var weight = weights[i];
weights[i] = new BoneWeight(
weight.index0,
weight.index1,
weight.index2,
weight.index3,
weight.weight0 * scale,
weight.weight1 * scale,
weight.weight2 * scale,
weight.weight3 * scale);
}
}
private static void NormalizeWeights(float[] weights)
{
var sum = weights.Sum();

View File

@@ -1,6 +1,7 @@
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.Services;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
@@ -19,6 +20,7 @@ public sealed class ModelDecimationService
private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly XivDataStorageService _xivDataStorageService;
private readonly ModelProcessingQueue _processingQueue;
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
private readonly TaskRegistry<string> _decimationDeduplicator = new();
@@ -30,13 +32,15 @@ public sealed class ModelDecimationService
LightlessConfigService configService,
FileCacheManager fileCacheManager,
PlayerPerformanceConfigService performanceConfigService,
XivDataStorageService xivDataStorageService)
XivDataStorageService xivDataStorageService,
ModelProcessingQueue processingQueue)
{
_logger = logger;
_configService = configService;
_fileCacheManager = fileCacheManager;
_performanceConfigService = performanceConfigService;
_xivDataStorageService = xivDataStorageService;
_processingQueue = processingQueue;
}
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
@@ -53,9 +57,9 @@ public sealed class ModelDecimationService
_logger.LogDebug("Queued model decimation for {Hash}", hash);
_decimationDeduplicator.GetOrStart(hash, async () =>
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
@@ -69,7 +73,7 @@ public sealed class ModelDecimationService
{
_decimationSemaphore.Release();
}
});
}, CancellationToken.None));
}
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
@@ -89,9 +93,9 @@ public sealed class ModelDecimationService
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
_decimationDeduplicator.GetOrStart(hash, async () =>
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
@@ -105,7 +109,7 @@ public sealed class ModelDecimationService
{
_decimationSemaphore.Release();
}
});
}, CancellationToken.None));
}
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class ModelProcessingQueue : IDisposable
{
private readonly AssetProcessingQueue _queue;
public ModelProcessingQueue(ILogger<ModelProcessingQueue> logger)
{
_queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing");
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
=> _queue.Enqueue(work, token);
public void Dispose()
=> _queue.Dispose();
}

View File

@@ -8,6 +8,7 @@ using System.Threading;
using OtterTex;
using OtterImage = OtterTex.Image;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Utils;
using LightlessSync.FileCache;
using Microsoft.Extensions.Logging;
@@ -33,6 +34,7 @@ public sealed class TextureDownscaleService
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly FileCacheManager _fileCacheManager;
private readonly TextureCompressionService _textureCompressionService;
private readonly TextureProcessingQueue _processingQueue;
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
@@ -73,13 +75,15 @@ public sealed class TextureDownscaleService
LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfigService,
FileCacheManager fileCacheManager,
TextureCompressionService textureCompressionService)
TextureCompressionService textureCompressionService,
TextureProcessingQueue processingQueue)
{
_logger = logger;
_configService = configService;
_playerPerformanceConfigService = playerPerformanceConfigService;
_fileCacheManager = fileCacheManager;
_textureCompressionService = textureCompressionService;
_processingQueue = processingQueue;
}
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
@@ -90,7 +94,7 @@ public sealed class TextureDownscaleService
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
_downscaleDeduplicator.GetOrStart(hash, async () =>
_downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{
TextureMapKind mapKind;
try
@@ -104,7 +108,7 @@ public sealed class TextureDownscaleService
}
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
});
}, CancellationToken.None));
}
public bool ShouldScheduleDownscale(string filePath)
@@ -382,6 +386,12 @@ public sealed class TextureDownscaleService
{
var isCompressed = sourceFormat.IsCompressed();
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
_logger.LogDebug(
"Downscale convert target {TargetFormat} (source {SourceFormat}, compressed {IsCompressed}, penumbraFallback {PenumbraFallback})",
targetFormat,
sourceFormat,
isCompressed,
attemptPenumbraFallback);
try
{
result = source.Convert(targetFormat);
@@ -433,6 +443,7 @@ public sealed class TextureDownscaleService
{
try
{
_logger.LogDebug("Downscale Penumbra re-encode target {Target} for {Hash}.", target, hash);
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
TexFileHelper.Save(destination, uncompressed);
}

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class TextureProcessingQueue : IDisposable
{
private readonly AssetProcessingQueue _queue;
public TextureProcessingQueue(ILogger<TextureProcessingQueue> logger)
{
_queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing");
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
=> _queue.Enqueue(work, token);
public void Dispose()
=> _queue.Dispose();
}

View File

@@ -56,6 +56,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly ModelDecimationService _modelDecimationService;
private readonly TextureCompressionService _textureCompressionService;
private readonly TextureMetadataHelper _textureMetadataHelper;
private readonly TextureProcessingQueue _processingQueue;
private readonly List<TextureRow> _textureRows = new();
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
@@ -137,7 +138,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService, ModelDecimationService modelDecimationService,
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper)
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper,
TextureProcessingQueue processingQueue)
: base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService)
{
_characterAnalyzer = characterAnalyzer;
@@ -150,6 +152,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_modelDecimationService = modelDecimationService;
_textureCompressionService = textureCompressionService;
_textureMetadataHelper = textureMetadataHelper;
_processingQueue = processingQueue;
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
{
_hasUpdate = true;
@@ -3716,7 +3719,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_conversionCurrentFileProgress = 0;
_conversionFailed = false;
_conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token);
var conversionToken = _conversionCancellationTokenSource.Token;
_conversionTask = _processingQueue.Enqueue(
queueToken => RunTextureConversionAsync(requests, queueToken),
conversionToken);
_showModal = true;
}

View File

@@ -0,0 +1,71 @@
using System.Runtime.InteropServices;
using static LightlessSync.Utils.PtrGuardMemory;
namespace LightlessSync.Utils
{
public static partial class PtrGuard
{
private const ulong _aligmentPtr = 0x7UL;
private static readonly nuint _minAppAddr = (nuint)GetMinAppAddr();
private static readonly nuint _maxAppAddr = (nuint)GetMaxAppAddr();
private static nint GetMinAppAddr()
{
GetSystemInfo(out var si);
return si.lpMinimumApplicationAddress;
}
private static nint GetMaxAppAddr()
{
GetSystemInfo(out var si);
return si.lpMaximumApplicationAddress;
}
public static bool LooksLikePtr(nint p)
{
if (p == 0) return false;
nuint u = (nuint)p;
if (u < _minAppAddr) return false;
if (u > _maxAppAddr) return false;
if ((u & _aligmentPtr) != 0) return false;
if ((uint)u == 0x12345679u) return false;
return true;
}
public static bool TryReadIntPtr(nint addr, out nint value)
{
value = 0;
if (!LooksLikePtr(addr))
return false;
return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead)
&& bytesRead == (nuint)IntPtr.Size;
}
public static bool IsReadable(nint addr, nuint size)
{
if (addr == 0 || size == 0) return false;
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
return false;
const uint Commit = 0x1000;
const uint NoAccess = 0x01;
const uint PageGuard = 0x100;
if (mbi.State != Commit) return false;
if ((mbi.Protect & PageGuard) != 0) return false;
if (mbi.Protect == NoAccess) return false;
ulong start = (ulong)addr;
ulong end = start + size - 1;
ulong r0 = (ulong)mbi.BaseAddress;
ulong r1 = r0 + mbi.RegionSize - 1;
return start >= r0 && end <= r1;
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Runtime.InteropServices;
namespace LightlessSync.Utils
{
internal static class PtrGuardMemory
{
[StructLayout(LayoutKind.Sequential)]
internal struct MEMORY_BASIC_INFORMATION
{
public nint BaseAddress;
public nint AllocationBase;
public uint AllocationProtect;
public nuint RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern nuint VirtualQuery(
nint lpAddress,
out MEMORY_BASIC_INFORMATION lpBuffer,
nuint dwLength);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool ReadProcessMemory(
nint hProcess,
nint lpBaseAddress,
out nint lpBuffer,
nuint nSize,
out nuint lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
internal static extern nint GetCurrentProcess();
[DllImport("kernel32.dll")]
internal static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo);
[StructLayout(LayoutKind.Sequential)]
internal struct SYSTEM_INFO
{
public ushort wProcessorArchitecture;
public ushort wReserved;
public uint dwPageSize;
public nint lpMinimumApplicationAddress;
public nint lpMaximumApplicationAddress;
public nint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public ushort wProcessorLevel;
public ushort wProcessorRevision;
}
}
}