diff --git a/OpenRA.Game/Widgets/MapPreviewWidget.cs b/OpenRA.Game/Widgets/MapPreviewWidget.cs index d68cf983d4..8effc6a3f6 100644 --- a/OpenRA.Game/Widgets/MapPreviewWidget.cs +++ b/OpenRA.Game/Widgets/MapPreviewWidget.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Threading; using OpenRA.FileFormats; using OpenRA.Graphics; @@ -26,8 +27,6 @@ namespace OpenRA.Widgets public bool IgnoreMouseInput = false; public bool ShowSpawnPoints = true; - static readonly Cache PreviewCache = new Cache(stub => Minimap.RenderMapPreview( new Map( stub.Path ))); - public MapPreviewWidget() : base() { } protected MapPreviewWidget(MapPreviewWidget other) @@ -68,19 +67,30 @@ namespace OpenRA.Widgets public override void Draw() { var map = Map(); - if( map == null ) return; + if (map == null) + return; + + // Preview unavailable + if (!Loaded) + { + GeneratePreview(); + return; + } if (lastMap != map) { lastMap = map; // Update image data - var preview = PreviewCache[map]; - if( mapChooserSheet == null || mapChooserSheet.Size.Width != preview.Width || mapChooserSheet.Size.Height != preview.Height ) - mapChooserSheet = new Sheet(new Size( preview.Width, preview.Height ) ); + Bitmap preview; + lock (syncRoot) + preview = Previews[map.Uid]; - mapChooserSheet.Texture.SetData( preview ); - mapChooserSprite = new Sprite( mapChooserSheet, new Rectangle( 0, 0, map.Bounds.Width, map.Bounds.Height ), TextureChannel.Alpha ); + if (mapChooserSheet == null || mapChooserSheet.Size.Width != preview.Width || mapChooserSheet.Size.Height != preview.Height) + mapChooserSheet = new Sheet(new Size(preview.Width, preview.Height)); + + mapChooserSheet.Texture.SetData(preview); + mapChooserSprite = new Sprite(mapChooserSheet, new Rectangle(0, 0, map.Bounds.Width, map.Bounds.Height), TextureChannel.Alpha); } // Update map rect @@ -90,9 +100,9 @@ namespace OpenRA.Widgets var dh = (int)(PreviewScale * (size - map.Bounds.Height)) / 2; MapRect = new Rectangle(RenderBounds.X + dw, RenderBounds.Y + dh, (int)(map.Bounds.Width * PreviewScale), (int)(map.Bounds.Height * PreviewScale)); - Game.Renderer.RgbaSpriteRenderer.DrawSprite( mapChooserSprite, + Game.Renderer.RgbaSpriteRenderer.DrawSprite(mapChooserSprite, new float2(MapRect.Location), - new float2( MapRect.Size ) ); + new float2(MapRect.Size)); if (ShowSpawnPoints) { @@ -119,15 +129,70 @@ namespace OpenRA.Widgets } } - /// - /// Forces loading the preview into the map cache. - /// - public Bitmap LoadMapPreview() - { - var map = Map(); - if( map == null ) return null; + // Async map preview generation bits + enum PreviewStatus { Invalid, Uncached, Generating, Cached } + static Thread previewLoaderThread; + static object syncRoot = new object(); + static Queue cacheUids = new Queue(); + static readonly Dictionary Previews = new Dictionary(); - return PreviewCache[map]; + void LoadAsyncInternal() + { + for (;;) + { + string uid; + lock (syncRoot) + { + if (cacheUids.Count == 0) + break; + uid = cacheUids.Peek(); + } + + var bitmap = Minimap.RenderMapPreview(Game.modData.AvailableMaps[uid]); + lock (syncRoot) + { + // TODO: We should add previews to a sheet here (with multiple previews per sheet) + Previews.Add(uid, bitmap); + cacheUids.Dequeue(); + } + } } + + void GeneratePreview() + { + var m = Map(); + if (m == null) + return; + + var status = Status(m); + if (status == PreviewStatus.Uncached) + lock (syncRoot) + cacheUids.Enqueue(m.Uid); + + if (previewLoaderThread == null || !previewLoaderThread.IsAlive) + { + previewLoaderThread = new Thread(LoadAsyncInternal); + previewLoaderThread.Priority = ThreadPriority.Lowest; + previewLoaderThread.Start(); + } + } + + static PreviewStatus Status(Map m) + { + if (m == null) + return PreviewStatus.Invalid; + + lock (syncRoot) + { + if (Previews.ContainsKey(m.Uid)) + return PreviewStatus.Cached; + + if (cacheUids.Contains(m.Uid)) + return PreviewStatus.Generating; + } + return PreviewStatus.Uncached; + } + + public bool Loaded { get { return Status(Map()) == PreviewStatus.Cached; } } } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs index 457c67830a..2324bf61f7 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs @@ -27,7 +27,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic ScrollPanelWidget scrollpanel; ScrollItemWidget itemTemplate; string gameMode; - Thread mapLoaderThread; [ObjectCreator.UseCtor] internal MapChooserLogic(Widget widget, string initialMap, Action onExit, Action onSelect) @@ -61,7 +60,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic { var item = ScrollItemWidget.Setup(template, () => gameMode == ii.First, - () => { gameMode = ii.First; EnumerateMapsAsync(); }); + () => { gameMode = ii.First; EnumerateMaps(); }); item.Get("LABEL").GetText = () => showItem(ii); return item; }; @@ -84,16 +83,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic randomMapButton.IsDisabled = () => visibleMaps == null || visibleMaps.Count == 0; } - EnumerateMapsAsync(); - } - - void EnumerateMapsAsync() - { - if (mapLoaderThread != null && mapLoaderThread.IsAlive) - mapLoaderThread.Abort(); // violent, but should be fine since we are not doing anything sensitive in this thread - - mapLoaderThread = new Thread(EnumerateMaps); - mapLoaderThread.Start(); + EnumerateMaps(); } void EnumerateMaps() @@ -104,7 +94,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic .OrderBy(kv => kv.Value.PlayerCount) .ThenBy(kv => kv.Value.Title); - var children = new List(); + scrollpanel.RemoveChildren(); foreach (var kv in maps) { var m = kv.Value; @@ -117,17 +107,20 @@ namespace OpenRA.Mods.RA.Widgets.Logic previewWidget.IgnoreMouseOver = true; previewWidget.IgnoreMouseInput = true; previewWidget.Map = () => m; - previewWidget.LoadMapPreview(); - var detailsWidget = item.Get("DETAILS"); + var previewLoadingWidget = item.GetOrNull("PREVIEW_PLACEHOLDER"); + if (previewLoadingWidget != null) + previewLoadingWidget.IsVisible = () => !previewWidget.Loaded; + + var detailsWidget = item.GetOrNull("DETAILS"); if (detailsWidget != null) detailsWidget.GetText = () => "{0} ({1})".F(m.Type, m.PlayerCount); - var authorWidget = item.Get("AUTHOR"); + var authorWidget = item.GetOrNull("AUTHOR"); if (authorWidget != null) authorWidget.GetText = () => m.Author; - var sizeWidget = item.Get("SIZE"); + var sizeWidget = item.GetOrNull("SIZE"); if (sizeWidget != null) { var size = m.Bounds.Width + "x" + m.Bounds.Height; @@ -139,20 +132,12 @@ namespace OpenRA.Mods.RA.Widgets.Logic sizeWidget.GetText = () => size; } - children.Add(item); + scrollpanel.AddChild(item); } - Game.RunAfterTick(() => - { - scrollpanel.RemoveChildren(); - - foreach (var c in children) - scrollpanel.AddChild(c); - - visibleMaps = maps.ToDictionary(kv => kv.Key, kv => kv.Value); - if (visibleMaps.ContainsValue(map)) - scrollpanel.ScrollToItem(visibleMaps.First(m => m.Value == map).Key); - }); + visibleMaps = maps.ToDictionary(kv => kv.Key, kv => kv.Value); + if (visibleMaps.ContainsValue(map)) + scrollpanel.ScrollToItem(visibleMaps.First(m => m.Value == map).Key); } } } diff --git a/mods/cnc/chrome/mapchooser.yaml b/mods/cnc/chrome/mapchooser.yaml index 13c6f51f79..325b967e20 100644 --- a/mods/cnc/chrome/mapchooser.yaml +++ b/mods/cnc/chrome/mapchooser.yaml @@ -43,6 +43,13 @@ Container@MAPCHOOSER_PANEL: Y:0 Visible:false Children: + Background@PREVIEW_PLACEHOLDER: + X:(PARENT_RIGHT - WIDTH)/2 + Y:4 + Width:158 + Height:158 + Background:panel-black + ClickThrough: false MapPreview@PREVIEW: X:(PARENT_RIGHT - WIDTH)/2 Y:4