mirror of
https://github.com/fadden/6502bench.git
synced 2024-11-25 14:34:27 +00:00
Add a progress bar to HTML export
Generation of HTML is extremely fast, but compressing thousands of frames for wireframe animated GIFs can take a little while. Sharing bitmaps between threads required two changes: (1) bitmaps need to be "frozen" after being drawn; (2) you can't use Path because BackgroundWorker isn't a STAThread. You can, however, use a DrawingVisual / DrawingContext to do the rendering. Which is really what I should have been doing all along; I just didn't know the approach existed until I was forced to go looking for it. Also, we now do a "run finalizers" call before generating an animated GIF. Without it things explode after more than 10K GDI objects have been allocated.
This commit is contained in:
parent
b3dacc2613
commit
1da98d8628
@ -107,7 +107,10 @@ namespace CommonWPF {
|
||||
int percent = e.ProgressPercentage;
|
||||
string msg = e.UserState as string;
|
||||
|
||||
Debug.Assert(percent >= 0 && percent <= 100);
|
||||
if (percent < 0 || percent > 100) {
|
||||
Debug.WriteLine("WorkProgress: bad percent " + percent);
|
||||
percent = 0;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(msg)) {
|
||||
messageText.Text = msg;
|
||||
|
@ -318,7 +318,7 @@ namespace SourceGen.AsmGen.WpfGui {
|
||||
}
|
||||
|
||||
private class AsmWorker : WorkProgress.IWorker {
|
||||
IAssembler mAssembler;
|
||||
private IAssembler mAssembler;
|
||||
public AssemblerResults Results { get; private set; }
|
||||
|
||||
public AsmWorker(IAssembler asm) {
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
@ -446,13 +447,45 @@ namespace SourceGen {
|
||||
private const string HTML_EXPORT_CSS_FILE = "SGStyle.css";
|
||||
private const string LABEL_LINK_PREFIX = "Sym";
|
||||
|
||||
private class ExportWorker : WorkProgress.IWorker {
|
||||
private Exporter mExporter;
|
||||
private string mPathName;
|
||||
private bool mOverwriteCss;
|
||||
|
||||
public bool Success { get; private set; }
|
||||
|
||||
public ExportWorker(Exporter exp, string pathName, bool overwriteCss) {
|
||||
mExporter = exp;
|
||||
mPathName = pathName;
|
||||
mOverwriteCss = overwriteCss;
|
||||
}
|
||||
public object DoWork(BackgroundWorker worker) {
|
||||
return mExporter.OutputToHtml(worker, mPathName, mOverwriteCss);
|
||||
}
|
||||
public void RunWorkerCompleted(object results) {
|
||||
if (results != null) {
|
||||
Success = (bool)results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates HTML output to the specified path.
|
||||
/// </summary>
|
||||
/// <param name="pathName">Full pathname of output file (including ".html"). This
|
||||
/// defines the root directory if there are additional files.</param>
|
||||
/// <param name="overwriteCss">If set, existing CSS file will be replaced.</param>
|
||||
public void OutputToHtml(string pathName, bool overwriteCss) {
|
||||
public void OutputToHtml(Window parent, string pathName, bool overwriteCss) {
|
||||
ExportWorker ew = new ExportWorker(this, pathName, overwriteCss);
|
||||
WorkProgress dlg = new WorkProgress(parent, ew, false);
|
||||
if (dlg.ShowDialog() != true) {
|
||||
Debug.WriteLine("Export unsuccessful");
|
||||
} else {
|
||||
Debug.WriteLine("Export complete");
|
||||
}
|
||||
}
|
||||
|
||||
private bool OutputToHtml(BackgroundWorker worker, string pathName, bool overwriteCss) {
|
||||
string exportTemplate = RuntimeDataAccess.GetPathName(HTML_EXPORT_TEMPLATE);
|
||||
string tmplStr;
|
||||
try {
|
||||
@ -463,7 +496,7 @@ namespace SourceGen {
|
||||
pathName, ex.Message);
|
||||
MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION,
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should only need the _IMG directory if there are visualizations.
|
||||
@ -478,7 +511,7 @@ namespace SourceGen {
|
||||
imageDirPath);
|
||||
MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION,
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
exists = true;
|
||||
} catch (FileNotFoundException) {
|
||||
@ -493,7 +526,7 @@ namespace SourceGen {
|
||||
imageDirPath, ex.Message);
|
||||
MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION,
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -501,6 +534,13 @@ namespace SourceGen {
|
||||
mImageDirPath = imageDirPath;
|
||||
}
|
||||
|
||||
if (mImageDirPath == null) {
|
||||
worker.ReportProgress(0, Res.Strings.EXPORTING_HTML);
|
||||
} else {
|
||||
worker.ReportProgress(0, Res.Strings.EXPORTING_HTML_AND_IMAGES);
|
||||
}
|
||||
|
||||
|
||||
// Perform some quick substitutions. This could be done more efficiently,
|
||||
// but we're only doing this on the template file, which should be small.
|
||||
tmplStr = tmplStr.Replace("$ProjectName$", mProject.DataFileName);
|
||||
@ -527,11 +567,13 @@ namespace SourceGen {
|
||||
int splitPoint = tmplStr.IndexOf(CodeLinesStr);
|
||||
if (splitPoint < 0) {
|
||||
Debug.WriteLine("No place to put code");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
string template1 = tmplStr.Substring(0, splitPoint);
|
||||
string template2 = tmplStr.Substring(splitPoint + CodeLinesStr.Length);
|
||||
|
||||
int lastProgressPerc = 0;
|
||||
|
||||
// Generate UTF-8 text, without a byte-order mark.
|
||||
using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) {
|
||||
sw.Write(template1);
|
||||
@ -545,12 +587,27 @@ namespace SourceGen {
|
||||
}
|
||||
|
||||
GenerateHtmlLine(lineIndex, sw, sb);
|
||||
|
||||
if (worker.CancellationPending) {
|
||||
break;
|
||||
}
|
||||
int perc = (lineIndex * 100) / mCodeLineList.Count;
|
||||
if (perc != lastProgressPerc) {
|
||||
lastProgressPerc = perc;
|
||||
worker.ReportProgress(perc);
|
||||
}
|
||||
}
|
||||
sw.WriteLine("</pre>\r\n");
|
||||
|
||||
sw.Write(template2);
|
||||
}
|
||||
|
||||
if (worker.CancellationPending) {
|
||||
Debug.WriteLine("Cancel requested, deleting " + pathName);
|
||||
File.Delete(pathName);
|
||||
return false;
|
||||
}
|
||||
|
||||
string cssFile = RuntimeDataAccess.GetPathName(HTML_EXPORT_CSS_FILE);
|
||||
string outputDir = Path.GetDirectoryName(pathName);
|
||||
string outputPath = Path.Combine(outputDir, HTML_EXPORT_CSS_FILE);
|
||||
@ -563,9 +620,11 @@ namespace SourceGen {
|
||||
cssFile, outputPath, ex.Message);
|
||||
MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION,
|
||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -2345,20 +2345,20 @@ namespace SourceGen {
|
||||
eport.Selection = selection;
|
||||
}
|
||||
|
||||
// This is generally fast enough that I don't feel the need to create a
|
||||
// progress window.
|
||||
if (dlg.GenType == WpfGui.Export.GenerateFileType.Html) {
|
||||
// Generating wireframe animations can be slow, so we need to use a
|
||||
// progress dialog.
|
||||
eport.OutputToHtml(mMainWin, dlg.PathName, dlg.OverwriteCss);
|
||||
} else {
|
||||
// Text output is generally very fast. Put up a wait cursor just in case.
|
||||
try {
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
|
||||
if (dlg.GenType == WpfGui.Export.GenerateFileType.Html) {
|
||||
eport.OutputToHtml(dlg.PathName, dlg.OverwriteCss);
|
||||
} else {
|
||||
eport.OutputToText(dlg.PathName, dlg.TextModeCsv);
|
||||
}
|
||||
} finally {
|
||||
Mouse.OverrideCursor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Find() {
|
||||
FindBox dlg = new FindBox(mMainWin, mFindString);
|
||||
|
@ -81,6 +81,8 @@ limitations under the License.
|
||||
<system:String x:Key="str_ErrProjectSaveFail">Unable to save project file</system:String>
|
||||
<system:String x:Key="str_ErrTooLargeForPreview">[File was too large for preview window]</system:String>
|
||||
<system:String x:Key="str_ErrValueIncompatibleWithMask">Symbol value is incompatible with current multi-mask</system:String>
|
||||
<system:String x:Key="str_ExportingHtml">Exporting HTML...</system:String>
|
||||
<system:String x:Key="str_ExportingHtmlAndImages">Exporting HTML and images...</system:String>
|
||||
<system:String x:Key="str_ExternalFileBadDirFmt" xml:space="preserve">Symbol files and extension scripts must live in the application runtime directory ({0}) or project directory ({1}).

File {2} lives elsewhere.</system:String>
|
||||
<system:String x:Key="str_ExternalFileBadDirCaption">File Not In Runtime Directory</system:String>
|
||||
<system:String x:Key="str_FileFilterAll">All files (*.*)|*.*</system:String>
|
||||
|
@ -143,6 +143,10 @@ namespace SourceGen.Res {
|
||||
(string)Application.Current.FindResource("str_ErrTooLargeForPreview");
|
||||
public static string ERR_VALUE_INCOMPATIBLE_WITH_MASK =
|
||||
(string)Application.Current.FindResource("str_ErrValueIncompatibleWithMask");
|
||||
public static string EXPORTING_HTML =
|
||||
(string)Application.Current.FindResource("str_ExportingHtml");
|
||||
public static string EXPORTING_HTML_AND_IMAGES =
|
||||
(string)Application.Current.FindResource("str_ExportingHtmlAndImages");
|
||||
public static string EXTERNAL_FILE_BAD_DIR_FMT =
|
||||
(string)Application.Current.FindResource("str_ExternalFileBadDirFmt");
|
||||
public static string EXTERNAL_FILE_BAD_DIR_CAPTION =
|
||||
|
@ -79,8 +79,13 @@ namespace SourceGen {
|
||||
public override void SetThumbnail(IVisualizationWireframe visWire,
|
||||
ReadOnlyDictionary<string, object> parms) {
|
||||
base.SetThumbnail(visWire, parms);
|
||||
if (visWire == null) {
|
||||
// Thumbnail cache is being cleared. Throw out the wireframe object too.
|
||||
mWireObj = null;
|
||||
} else {
|
||||
mWireObj = WireframeObject.Create(visWire);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an animated GIF from a series of frames.
|
||||
@ -99,6 +104,20 @@ namespace SourceGen {
|
||||
bool doPersp = Util.GetFromObjDict(VisGenParams, VisWireframe.P_IS_PERSPECTIVE, true);
|
||||
bool doBfc = Util.GetFromObjDict(VisGenParams, VisWireframe.P_IS_BFC_ENABLED, false);
|
||||
|
||||
// Try to avoid System.Runtime.InteropServices.COMException (0x88980003):
|
||||
// MILERR_WIN32ERROR (Exception from HRESULT: 0x88980003)
|
||||
// The problem seems to be that the bitmaps are GDI handles, there's a hard limit of
|
||||
// 10,000, and they don't get released until finalizers run. If we wait for
|
||||
// pending finalizers the pool stays at a manageable level. If we poke the GC
|
||||
// every time the memory graph looks better but I suspect there's a performance hit.
|
||||
//GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
if (mWireObj == null) {
|
||||
Debug.WriteLine("EncodeGif: wire obj is null");
|
||||
frameCount = 1;
|
||||
}
|
||||
|
||||
for (int frame = 0; frame < frameCount; frame++) {
|
||||
BitmapSource bs = GenerateWireframeImage(mWireObj, dim,
|
||||
curX, curY, curZ, doPersp, doBfc);
|
||||
|
@ -106,8 +106,11 @@ namespace SourceGen {
|
||||
/// <summary>
|
||||
/// Image to show when things are broken.
|
||||
/// </summary>
|
||||
public static readonly BitmapImage BROKEN_IMAGE =
|
||||
new BitmapImage(new Uri("pack://application:,,,/Res/RedX.png"));
|
||||
public static readonly BitmapImage BROKEN_IMAGE;
|
||||
static Visualization() {
|
||||
BROKEN_IMAGE = new BitmapImage(new Uri("pack://application:,,,/Res/RedX.png"));
|
||||
BROKEN_IMAGE.Freeze();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Image to overlay on animation visualizations.
|
||||
@ -185,20 +188,25 @@ namespace SourceGen {
|
||||
} else {
|
||||
CachedImage = ConvertToBitmapSource(vis2d);
|
||||
}
|
||||
Debug.Assert(CachedImage.IsFrozen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the cached thumbnail image.
|
||||
/// </summary>
|
||||
/// <param name="visWire">Visualization object.</param>
|
||||
/// <param name="visWire">Visualization object, or null to clear the thumbnail.</param>
|
||||
/// <param name="parms">Visualization parameters.</param>
|
||||
public virtual void SetThumbnail(IVisualizationWireframe visWire,
|
||||
ReadOnlyDictionary<string, object> parms) {
|
||||
Debug.Assert(visWire != null);
|
||||
if (visWire == null) {
|
||||
CachedImage = BROKEN_IMAGE;
|
||||
} else {
|
||||
Debug.Assert(parms != null);
|
||||
WireframeObject wireObj = WireframeObject.Create(visWire);
|
||||
CachedImage = GenerateWireframeImage(wireObj, THUMBNAIL_DIM, parms);
|
||||
}
|
||||
Debug.Assert(CachedImage.IsFrozen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims a tag, removing leading/trailing whitespace, and checks it for validity.
|
||||
@ -246,6 +254,7 @@ namespace SourceGen {
|
||||
palette,
|
||||
vis2d.GetPixels(),
|
||||
vis2d.Width);
|
||||
image.Freeze();
|
||||
|
||||
return image;
|
||||
}
|
||||
@ -274,11 +283,17 @@ namespace SourceGen {
|
||||
/// </summary>
|
||||
public static BitmapSource GenerateWireframeImage(WireframeObject wireObj,
|
||||
double dim, int eulerX, int eulerY, int eulerZ, bool doPersp, bool doBfc) {
|
||||
if (wireObj == null) {
|
||||
// Can happen if the visualization generator is failing on stuff loaded from
|
||||
// the project file.
|
||||
return BROKEN_IMAGE;
|
||||
}
|
||||
|
||||
// Generate the path geometry.
|
||||
GeometryGroup geo = GenerateWireframePath(wireObj, dim, eulerX, eulerY, eulerZ,
|
||||
doPersp, doBfc);
|
||||
|
||||
// Render Path to bitmap -- https://stackoverflow.com/a/23582564/294248
|
||||
// Render geometry to bitmap -- https://stackoverflow.com/a/869767/294248
|
||||
Rect bounds = geo.GetRenderBounds(null);
|
||||
|
||||
//Debug.WriteLine("RenderWF dim=" + dim + " bounds=" + bounds + ": " + wireObj);
|
||||
@ -290,7 +305,18 @@ namespace SourceGen {
|
||||
96,
|
||||
96,
|
||||
PixelFormats.Pbgra32);
|
||||
//RenderOptions.SetEdgeMode(bitmap, EdgeMode.Aliased); <-- doesn't work?
|
||||
//RenderOptions.SetEdgeMode(bitmap, EdgeMode.Aliased); <-- no apparent effect
|
||||
|
||||
DrawingVisual dv = new DrawingVisual();
|
||||
using (DrawingContext dc = dv.RenderOpen()) {
|
||||
dc.DrawRectangle(Brushes.Black, null, new Rect(0, 0, bounds.Width, bounds.Height));
|
||||
Pen pen = new Pen(Brushes.White, 1.0);
|
||||
dc.DrawGeometry(Brushes.White, pen, geo);
|
||||
}
|
||||
bitmap.Render(dv);
|
||||
|
||||
#if false
|
||||
// Old way: render Path to bitmap -- https://stackoverflow.com/a/23582564/294248
|
||||
|
||||
// Clear the bitmap to black. (Is there an easier way?)
|
||||
GeometryGroup bkgnd = new GeometryGroup();
|
||||
@ -308,7 +334,9 @@ namespace SourceGen {
|
||||
path.Measure(bounds.Size);
|
||||
path.Arrange(bounds);
|
||||
bitmap.Render(path);
|
||||
#endif
|
||||
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@ -405,6 +433,7 @@ namespace SourceGen {
|
||||
private static BitmapSource GenerateBlankImage() {
|
||||
RenderTargetBitmap bmp = new RenderTargetBitmap(1, 1, 96.0, 96.0,
|
||||
PixelFormats.Pbgra32);
|
||||
bmp.Freeze();
|
||||
return bmp;
|
||||
}
|
||||
|
||||
@ -438,6 +467,7 @@ namespace SourceGen {
|
||||
RenderTargetBitmap bmp = new RenderTargetBitmap(IMAGE_SIZE, IMAGE_SIZE, 96.0, 96.0,
|
||||
PixelFormats.Pbgra32);
|
||||
bmp.Render(visual);
|
||||
bmp.Freeze();
|
||||
return bmp;
|
||||
}
|
||||
|
||||
|
@ -167,6 +167,7 @@ namespace SourceGen {
|
||||
public void RefreshNeeded() {
|
||||
foreach (Visualization vis in mList) {
|
||||
vis.SetThumbnail(null);
|
||||
vis.SetThumbnail(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user