All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
312 lines
14 KiB
C#
312 lines
14 KiB
C#
using LightlessSync.API.Data;
|
|
using LightlessSync.FileCache;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using LightlessSync.Services.Events;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Services.ModelDecimation;
|
|
using LightlessSync.Services.TextureCompression;
|
|
using LightlessSync.UI;
|
|
using LightlessSync.WebAPI.Files.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LightlessSync.Services;
|
|
|
|
public class PlayerPerformanceService
|
|
{
|
|
private readonly FileCacheManager _fileCacheManager;
|
|
private readonly XivDataAnalyzer _xivDataAnalyzer;
|
|
private readonly ILogger<PlayerPerformanceService> _logger;
|
|
private readonly LightlessMediator _mediator;
|
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
|
private readonly ModelDecimationService _modelDecimationService;
|
|
private readonly TextureDownscaleService _textureDownscaleService;
|
|
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
|
|
|
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
|
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
|
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService,
|
|
ModelDecimationService modelDecimationService)
|
|
{
|
|
_logger = logger;
|
|
_mediator = mediator;
|
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
|
_fileCacheManager = fileCacheManager;
|
|
_xivDataAnalyzer = xivDataAnalyzer;
|
|
_textureDownscaleService = textureDownscaleService;
|
|
_modelDecimationService = modelDecimationService;
|
|
}
|
|
|
|
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
|
{
|
|
var config = _playerPerformanceConfigService.Current;
|
|
bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []);
|
|
if (!notPausedAfterVram) return false;
|
|
bool notPausedAfterTris = await CheckTriangleUsageThresholds(pairHandler, charaData).ConfigureAwait(false);
|
|
if (!notPausedAfterTris) return false;
|
|
|
|
if (config.UIDsToIgnore
|
|
.Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal)))
|
|
return true;
|
|
|
|
|
|
var vramUsage = pairHandler.LastAppliedApproximateVRAMBytes;
|
|
var triUsage = pairHandler.LastAppliedDataTris;
|
|
|
|
bool isPrefPerm = pairHandler.HasStickyPermissions;
|
|
|
|
bool exceedsTris = CheckForThreshold(config.WarnOnExceedingThresholds, config.TrisWarningThresholdThousands * 1000L,
|
|
triUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm);
|
|
bool exceedsVram = CheckForThreshold(config.WarnOnExceedingThresholds, config.VRAMSizeWarningThresholdMiB * 1024L * 1024L,
|
|
vramUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm);
|
|
|
|
if (_warnedForPlayers.TryGetValue(pairHandler.UserData.UID, out bool hadWarning) && hadWarning)
|
|
{
|
|
_warnedForPlayers[pairHandler.UserData.UID] = exceedsTris || exceedsVram;
|
|
return true;
|
|
}
|
|
|
|
_warnedForPlayers[pairHandler.UserData.UID] = exceedsTris || exceedsVram;
|
|
|
|
if (exceedsVram)
|
|
{
|
|
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
|
$"Exceeds VRAM threshold: ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeWarningThresholdMiB} MiB)")));
|
|
}
|
|
|
|
if (exceedsTris)
|
|
{
|
|
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
|
$"Exceeds triangle threshold: ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
|
|
}
|
|
|
|
if (exceedsTris || exceedsVram)
|
|
{
|
|
string warningText = string.Empty;
|
|
if (exceedsTris && !exceedsVram)
|
|
{
|
|
warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" +
|
|
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
|
|
}
|
|
else if (!exceedsTris)
|
|
{
|
|
warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" +
|
|
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB";
|
|
}
|
|
else
|
|
{
|
|
warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds both VRAM warning threshold and triangle warning threshold\n" +
|
|
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB and {triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
|
|
}
|
|
|
|
_mediator.Publish(new PerformanceNotificationMessage(
|
|
$"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds performance threshold(s)",
|
|
warningText,
|
|
pairHandler.UserData,
|
|
pairHandler.IsPaused,
|
|
pairHandler.PlayerName));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> CheckTriangleUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
|
{
|
|
var config = _playerPerformanceConfigService.Current;
|
|
|
|
long triUsage = 0;
|
|
long effectiveTriUsage = 0;
|
|
|
|
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
|
{
|
|
pairHandler.LastAppliedDataTris = 0;
|
|
pairHandler.LastAppliedApproximateEffectiveTris = 0;
|
|
return true;
|
|
}
|
|
|
|
var moddedModelHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase)))
|
|
.Select(p => p.Hash)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
|
|
|
foreach (var hash in moddedModelHashes)
|
|
{
|
|
var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
|
|
triUsage += tris;
|
|
|
|
long effectiveTris = tris;
|
|
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
|
|
if (fileEntry != null)
|
|
{
|
|
var preferredPath = fileEntry.ResolvedFilepath;
|
|
if (!skipDecimation)
|
|
{
|
|
preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
|
|
}
|
|
|
|
if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false);
|
|
if (decimatedTris > 0)
|
|
{
|
|
effectiveTris = decimatedTris;
|
|
}
|
|
}
|
|
}
|
|
|
|
effectiveTriUsage += effectiveTris;
|
|
}
|
|
|
|
pairHandler.LastAppliedDataTris = triUsage;
|
|
pairHandler.LastAppliedApproximateEffectiveTris = effectiveTriUsage;
|
|
|
|
_logger.LogDebug("Calculated triangle usage for {p}", pairHandler);
|
|
|
|
// no warning of any kind on ignored pairs
|
|
if (config.UIDsToIgnore
|
|
.Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal)))
|
|
return true;
|
|
|
|
bool isPrefPerm = pairHandler.HasStickyPermissions;
|
|
|
|
// now check auto pause
|
|
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000L,
|
|
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
|
{
|
|
var message = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold and has been automatically paused\n" +
|
|
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles";
|
|
|
|
_mediator.Publish(new PerformanceNotificationMessage(
|
|
$"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) automatically paused",
|
|
message,
|
|
pairHandler.UserData,
|
|
true,
|
|
pairHandler.PlayerName));
|
|
|
|
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
|
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
|
|
|
|
_mediator.Publish(new PauseMessage(pairHandler.UserData));
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
|
{
|
|
var config = _playerPerformanceConfigService.Current;
|
|
bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs
|
|
&& pairHandler.IsDirectlyPaired
|
|
&& pairHandler.HasStickyPermissions;
|
|
|
|
long vramUsage = 0;
|
|
long effectiveVramUsage = 0;
|
|
|
|
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
|
{
|
|
pairHandler.LastAppliedApproximateVRAMBytes = 0;
|
|
pairHandler.LastAppliedApproximateEffectiveVRAMBytes = 0;
|
|
return true;
|
|
}
|
|
|
|
var moddedTextureHashes = playerReplacements.Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)))
|
|
.Select(p => p.Hash)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
foreach (var hash in moddedTextureHashes)
|
|
{
|
|
long fileSize = 0;
|
|
long effectiveSize = 0;
|
|
|
|
var download = toDownloadFiles.Find(f => string.Equals(hash, f.Hash, StringComparison.OrdinalIgnoreCase));
|
|
if (download != null)
|
|
{
|
|
fileSize = download.TotalRaw;
|
|
effectiveSize = fileSize;
|
|
}
|
|
else
|
|
{
|
|
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
|
|
if (fileEntry == null) continue;
|
|
|
|
if (fileEntry.Size == null)
|
|
{
|
|
fileEntry.Size = new FileInfo(fileEntry.ResolvedFilepath).Length;
|
|
_fileCacheManager.UpdateHashedFile(fileEntry, computeProperties: true);
|
|
}
|
|
|
|
fileSize = fileEntry.Size.Value;
|
|
effectiveSize = fileSize;
|
|
|
|
if (!skipDownscale)
|
|
{
|
|
var preferredPath = _textureDownscaleService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
|
|
if (!string.IsNullOrEmpty(preferredPath) && File.Exists(preferredPath))
|
|
{
|
|
try
|
|
{
|
|
effectiveSize = new FileInfo(preferredPath).Length;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogTrace(ex, "Failed to read size for preferred texture path {Path}", preferredPath);
|
|
effectiveSize = fileSize;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
effectiveSize = fileSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
vramUsage += fileSize;
|
|
effectiveVramUsage += effectiveSize;
|
|
}
|
|
|
|
pairHandler.LastAppliedApproximateVRAMBytes = vramUsage;
|
|
pairHandler.LastAppliedApproximateEffectiveVRAMBytes = effectiveVramUsage;
|
|
|
|
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
|
|
|
|
// no warning of any kind on ignored pairs
|
|
if (config.UIDsToIgnore
|
|
.Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal)))
|
|
return true;
|
|
|
|
bool isPrefPerm = pairHandler.HasStickyPermissions;
|
|
|
|
// now check auto pause
|
|
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024L * 1024L,
|
|
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
|
{
|
|
var message = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold and has been automatically paused\n" +
|
|
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB";
|
|
|
|
_mediator.Publish(new PerformanceNotificationMessage(
|
|
$"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) automatically paused",
|
|
message,
|
|
pairHandler.UserData,
|
|
true,
|
|
pairHandler.PlayerName));
|
|
|
|
_mediator.Publish(new PauseMessage(pairHandler.UserData));
|
|
|
|
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
|
$"Exceeds VRAM threshold: automatically paused ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB} MiB)")));
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
|
|
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
|
|
}
|