Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache icon renders between frames #1825

Merged
merged 8 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion OpenDreamClient/Input/ContextMenu/ContextMenuItem.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public ContextMenuItem(ContextMenuPopup menu, ClientObjectReference target, Meta
NameLabel.Margin = new Thickness(2, 0, 4, 0);
NameLabel.Text = metadata.EntityName;

Icon.Texture = sprite.Icon.CurrentFrame;
Icon.Texture = sprite.Icon.CachedTexture;
}

protected override void MouseEntered() {
Expand Down
3 changes: 2 additions & 1 deletion OpenDreamClient/Rendering/ClientAppearanceSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal sealed class ClientAppearanceSystem : SharedAppearanceSystem {
[Dependency] private readonly IDreamResourceManager _dreamResourceManager = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IClyde _clyde = default!;

public override void Initialize() {
SubscribeNetworkEvent<AllAppearancesEvent>(OnAllAppearances);
Expand Down Expand Up @@ -49,7 +50,7 @@ public DreamIcon GetTurfIcon(int turfId) {
int appearanceId = turfId - 1;

if (!_turfIcons.TryGetValue(appearanceId, out var icon)) {
icon = new DreamIcon(_gameTiming, this, appearanceId);
icon = new DreamIcon(_gameTiming, _clyde, this, appearanceId);
_turfIcons.Add(appearanceId, icon);
}

Expand Down
9 changes: 8 additions & 1 deletion OpenDreamClient/Rendering/DMISpriteSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using OpenDreamShared.Rendering;
using Robust.Client.Graphics;
using Robust.Shared.GameStates;
using Robust.Shared.Timing;

Expand All @@ -9,10 +10,12 @@ public sealed class DMISpriteSystem : EntitySystem {
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly IClyde _clyde = default!;

public override void Initialize() {
SubscribeLocalEvent<DMISpriteComponent, ComponentAdd>(HandleComponentAdd);
SubscribeLocalEvent<DMISpriteComponent, ComponentHandleState>(HandleComponentState);
SubscribeLocalEvent<DMISpriteComponent, ComponentRemove>(HandleComponentRemove);
}

private void OnIconSizeChanged(EntityUid uid) {
Expand All @@ -21,7 +24,7 @@ private void OnIconSizeChanged(EntityUid uid) {
}

private void HandleComponentAdd(EntityUid uid, DMISpriteComponent component, ref ComponentAdd args) {
component.Icon = new DreamIcon(_gameTiming, _appearanceSystem);
component.Icon = new DreamIcon(_gameTiming, _clyde, _appearanceSystem);
component.Icon.SizeChanged += () => OnIconSizeChanged(uid);
}

Expand All @@ -33,4 +36,8 @@ private static void HandleComponentState(EntityUid uid, DMISpriteComponent compo
component.ScreenLocation = state.ScreenLocation;
component.Icon.SetAppearance(state.AppearanceId);
}

private static void HandleComponentRemove(EntityUid uid, DMISpriteComponent component, ref ComponentRemove args) {
component.Icon.Dispose();
}
}
105 changes: 97 additions & 8 deletions OpenDreamClient/Rendering/DreamIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace OpenDreamClient.Rendering;

internal sealed class DreamIcon(IGameTiming gameTiming, ClientAppearanceSystem appearanceSystem) {
internal sealed class DreamIcon(IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem appearanceSystem) : IDisposable {
public delegate void SizeChangedEventHandler();

public List<DreamIcon> Overlays { get; } = new();
Expand All @@ -34,26 +34,61 @@ public int AnimationFrame {
public IconAppearance? Appearance {
get => CalculateAnimatedAppearance();
private set {
if (_appearance?.Equals(value) is true)
return;

_appearance = value;
UpdateIcon();
}
}
private IconAppearance? _appearance;

public AtlasTexture? CurrentFrame => (Appearance == null || DMI == null)
? null
: DMI.GetState(Appearance.IconState)?.GetFrames(Appearance.Direction)[AnimationFrame];
// TODO: We could cache these per-appearance instead of per-atom
public Texture? CachedTexture { get; private set; }
public Vector2 TextureRenderOffset = Vector2.Zero;

private int _animationFrame;
private TimeSpan _animationFrameTime = gameTiming.CurTime;
private List<AppearanceAnimation>? _appearanceAnimations;
private Box2? _cachedAABB;
private IRenderTexture? _ping, _pong;
private bool _textureDirty = true;

public DreamIcon(IGameTiming gameTiming, ClientAppearanceSystem appearanceSystem, int appearanceId,
AtomDirection? parentDir = null) : this(gameTiming, appearanceSystem) {
public DreamIcon(IGameTiming gameTiming, IClyde clyde, ClientAppearanceSystem appearanceSystem, int appearanceId,
AtomDirection? parentDir = null) : this(gameTiming, clyde, appearanceSystem) {
SetAppearance(appearanceId, parentDir);
}

public void Dispose() {
_ping?.Dispose();
_pong?.Dispose();
}

public Texture? GetTexture(DreamViewOverlay viewOverlay, DrawingHandleWorld handle, RendererMetaData iconMetaData, Texture? textureOverride = null) {
if (Appearance == null || DMI == null)
return textureOverride;

var animationFrame = AnimationFrame;
if (textureOverride == null && CachedTexture != null && !_textureDirty)
return CachedTexture;

var frame = textureOverride ?? DMI.GetState(Appearance.IconState)?.GetFrames(Appearance.Direction)[animationFrame];
if (frame == null) {
CachedTexture = null;
} else if ((Appearance.Filters.Count == 0 && iconMetaData.ColorToApply == Color.White &&
iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity)) && iconMetaData.AlphaToApply.Equals(1.0f)) {
TextureRenderOffset = Vector2.Zero;
CachedTexture = frame;
} else {
CachedTexture = FullRenderTexture(viewOverlay, handle, iconMetaData, frame);
}

if (textureOverride == null)
_textureDirty = false;

return CachedTexture;
}

public void SetAppearance(int? appearanceId, AtomDirection? parentDir = null) {
// End any animations that are currently happening
// Note that this isn't faithful to the original behavior
Expand Down Expand Up @@ -138,13 +173,15 @@ private void UpdateAnimation() {
return;
DMIParser.ParsedDMIFrame[] frames = dmiState.GetFrames(Appearance.Direction);

if (frames.Length <= 1) return;
if (_animationFrame == frames.Length - 1 && !dmiState.Loop) return;

TimeSpan elapsedTime = gameTiming.CurTime.Subtract(_animationFrameTime);
while (elapsedTime >= frames[_animationFrame].Delay) {
elapsedTime -= frames[_animationFrame].Delay;
_animationFrameTime += frames[_animationFrame].Delay;
_animationFrame++;
_textureDirty = true;

if (_animationFrame >= frames.Length) _animationFrame -= frames.Length;
}
Expand Down Expand Up @@ -350,6 +387,8 @@ private void UpdateAnimation() {
}

private void UpdateIcon() {
_textureDirty = true;

if (Appearance == null) {
DMI = null;
return;
Expand All @@ -369,21 +408,71 @@ private void UpdateIcon() {

Overlays.Clear();
foreach (int overlayId in Appearance.Overlays) {
DreamIcon overlay = new DreamIcon(gameTiming, appearanceSystem, overlayId, Appearance.Direction);
DreamIcon overlay = new DreamIcon(gameTiming, clyde, appearanceSystem, overlayId, Appearance.Direction);
overlay.SizeChanged += CheckSizeChange;

Overlays.Add(overlay);
}

Underlays.Clear();
foreach (int underlayId in Appearance.Underlays) {
DreamIcon underlay = new DreamIcon(gameTiming, appearanceSystem, underlayId, Appearance.Direction);
DreamIcon underlay = new DreamIcon(gameTiming, clyde, appearanceSystem, underlayId, Appearance.Direction);
underlay.SizeChanged += CheckSizeChange;

Underlays.Add(underlay);
}
}

/// <summary>
/// Perform a full (slower) render of this icon's texture, including filters and color
/// </summary>
/// <remarks>In a separate method to avoid allocations when not executed</remarks>
/// <returns>The final texture</returns>
private Texture FullRenderTexture(DreamViewOverlay viewOverlay, DrawingHandleWorld handle, RendererMetaData iconMetaData, Texture frame) {
if (_ping?.Size != frame.Size * 2 || _pong == null) {
_ping?.Dispose();
_pong?.Dispose();

// TODO: This should determine the size from the filters and their settings, not just double the original
_ping = clyde.CreateRenderTarget(frame.Size * 2, new(RenderTargetColorFormat.Rgba8Srgb));
_pong = clyde.CreateRenderTarget(_ping.Size, new(RenderTargetColorFormat.Rgba8Srgb));
}

handle.RenderInRenderTarget(_pong, () => {
//we can use the color matrix shader here, since we don't need to blend
//also because blend mode is none, we don't need to clear
var colorMatrix = iconMetaData.ColorMatrixToApply.Equals(ColorMatrix.Identity)
? new ColorMatrix(iconMetaData.ColorToApply.WithAlpha(iconMetaData.AlphaToApply))
: iconMetaData.ColorMatrixToApply;

ShaderInstance colorShader = DreamViewOverlay.ColorInstance.Duplicate();
colorShader.SetParameter("colorMatrix", colorMatrix.GetMatrix4());
colorShader.SetParameter("offsetVector", colorMatrix.GetOffsetVector());
colorShader.SetParameter("isPlaneMaster",iconMetaData.IsPlaneMaster);
handle.UseShader(colorShader);

handle.SetTransform(DreamViewOverlay.CreateRenderTargetFlipMatrix(_pong.Size, frame.Size / 2));
handle.DrawTextureRect(frame, new Box2(Vector2.Zero, frame.Size));
}, Color.Black.WithAlpha(0));

foreach (DreamFilter filterId in iconMetaData.MainIcon!.Appearance!.Filters) {
ShaderInstance s = appearanceSystem.GetFilterShader(filterId, viewOverlay.RenderSourceLookup);

handle.RenderInRenderTarget(_ping, () => {
handle.UseShader(s);

// Technically this should be ping.Size, but they are the same size so avoid the extra closure alloc
handle.SetTransform(DreamViewOverlay.CreateRenderTargetFlipMatrix(_pong.Size, Vector2.Zero));
handle.DrawTextureRect(_pong.Texture, new Box2(Vector2.Zero, _pong.Size));
}, Color.Black.WithAlpha(0));

(_ping, _pong) = (_pong, _ping);
}

TextureRenderOffset = -(_pong.Texture.Size / 2 - frame.Size / 2);
return _pong.Texture;
}

private void CheckSizeChange() {
Box2? aabb = null;
GetWorldAABB(Vector2.Zero, ref aabb);
Expand Down
2 changes: 1 addition & 1 deletion OpenDreamClient/Rendering/DreamPlane.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Ve
if (sprite.MouseOpacity == MouseOpacity.Transparent || sprite.ShouldPassMouse)
continue;

var texture = sprite.Texture;
var texture = sprite.GetTexture(overlay, handle);
if (texture == null)
continue;

Expand Down
Loading
Loading