#region ================== Copyright (c) 2007 Pascal vd Heiden /* * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com * This program is released under GNU General Public License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #endregion #region ================== Namespaces using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.IO; using CodeImp.DoomBuilder.Rendering; using CodeImp.DoomBuilder.Windows; using SlimDX; using SlimDX.Direct3D9; #endregion namespace CodeImp.DoomBuilder.Data { public abstract unsafe class ImageData : IDisposable { #region ================== Constants #endregion #region ================== Variables // Properties protected string name; protected long longname; protected int width; protected int height; protected Vector2D scale; protected bool worldpanning; private bool usecolorcorrection; protected string filepathname; //mxd. Absolute path to the image; protected string shortname; //mxd. Name in uppercase and clamped to DataManager.CLASIC_IMAGE_NAME_LENGTH protected string virtualname; //mxd. Path of this name is used in TextureBrowserForm protected string displayname; //mxd. Name to display in TextureBrowserForm protected bool isFlat; //mxd. If false, it's a texture protected bool istranslucent; //mxd. If true, has pixels with alpha > 0 && < 255 protected bool ismasked; //mxd. If true, has pixels with zero alpha protected bool hasLongName; //mxd. Texture name is longer than DataManager.CLASIC_IMAGE_NAME_LENGTH protected bool hasPatchWithSameName; //mxd protected int level; //mxd. Folder depth of this item //mxd. Hashing private static int hashcounter; private readonly int hashcode; // Loading private volatile ImageLoadState previewstate; private volatile ImageLoadState imagestate; private volatile int previewindex; protected volatile bool loadfailed; private volatile bool allowunload; // References private volatile bool usedinmap; private volatile int references; // GDI bitmap protected Bitmap bitmap; // Direct3D texture private int mipmaplevels; // 0 = all mipmaps protected bool dynamictexture; private Texture texture; // Disposing protected bool isdisposed; #endregion #region ================== Properties public string Name { get { return name; } } public long LongName { get { return longname; } } public string ShortName { get { return shortname; } } //mxd public string FilePathName { get { return filepathname; } } //mxd public string VirtualName { get { return virtualname; } } //mxd public string DisplayName { get { return displayname; } } //mxd public bool IsFlat { get { return isFlat; } } //mxd public bool IsTranslucent { get { return istranslucent; } } //mxd public bool IsMasked { get { return ismasked; } } //mxd public bool HasPatchWithSameName { get { return hasPatchWithSameName; } } //mxd internal bool HasLongName { get { return hasLongName; } } //mxd public bool UseColorCorrection { get { return usecolorcorrection; } set { usecolorcorrection = value; } } public Texture Texture { get { lock(this) { return texture; } } } public bool IsPreviewLoaded { get { return (previewstate == ImageLoadState.Ready); } } public bool IsImageLoaded { get { return (imagestate == ImageLoadState.Ready); } } public bool LoadFailed { get { return loadfailed; } } public bool IsDisposed { get { return isdisposed; } } public bool AllowUnload { get { return allowunload; } set { allowunload = value; } } public ImageLoadState ImageState { get { return imagestate; } internal set { imagestate = value; } } public ImageLoadState PreviewState { get { return previewstate; } internal set { previewstate = value; } } public bool IsReferenced { get { return (references > 0) || usedinmap; } } public bool UsedInMap { get { return usedinmap; } } public int MipMapLevels { get { return mipmaplevels; } set { mipmaplevels = value; } } public int Width { get { return width; } } public int Height { get { return height; } } internal int PreviewIndex { get { return previewindex; } set { previewindex = value; } } //mxd. Scaled texture size is integer in ZDoom. public float ScaledWidth { get { return (float)Math.Round(width * scale.x); } } public float ScaledHeight { get { return (float)Math.Round(height * scale.y); } } public Vector2D Scale { get { return scale; } } public bool WorldPanning { get { return worldpanning; } } public int Level { get { return level; } } //mxd #endregion #region ================== Constructor / Disposer // Constructor protected ImageData() { // Defaults usecolorcorrection = true; allowunload = true; //mxd. Hashing hashcode = hashcounter++; } // Destructor ~ImageData() { this.Dispose(); } // Disposer public virtual void Dispose() { // Not already disposed? if(!isdisposed) { lock(this) { // Clean up if(bitmap != null) bitmap.Dispose(); if(texture != null) texture.Dispose(); bitmap = null; texture = null; // Done usedinmap = false; imagestate = ImageLoadState.None; previewstate = ImageLoadState.None; isdisposed = true; } } } #endregion #region ================== Management // This sets the status of the texture usage in the map internal void SetUsedInMap(bool used) { if(used != usedinmap) { usedinmap = used; General.Map.Data.ProcessImage(this); } } // This adds a reference public void AddReference() { references++; if(references == 1) General.Map.Data.ProcessImage(this); } // This removes a reference public void RemoveReference() { references--; if(references < 0) General.Fail("FAIL! (references < 0) Somewhere this image is dereferenced more than it was referenced."); if(references == 0) General.Map.Data.ProcessImage(this); } // This sets the name protected virtual void SetName(string name) { this.name = name; this.filepathname = name; //mxd this.shortname = name; //mxd this.virtualname = name; //mxd this.displayname = name; //mxd this.longname = Lump.MakeLongName(name); //mxd } // This unloads the image public virtual void UnloadImage() { lock(this) { if(bitmap != null) bitmap.Dispose(); bitmap = null; imagestate = ImageLoadState.None; } } // This returns the bitmap image public Bitmap GetBitmap() { lock(this) { // Image loaded successfully? if(!loadfailed && (imagestate == ImageLoadState.Ready) && (bitmap != null)) return bitmap; // Image loading failed? return (loadfailed ? Properties.Resources.Failed : Properties.Resources.Hourglass); } } // This loads the image public virtual void LoadImage() { // Do the loading LocalLoadImage(); // Notify the main thread about the change so that sectors can update their buffers IntPtr strptr = Marshal.StringToCoTaskMemAuto(this.name); General.SendMessage(General.MainWindow.Handle, (int)MainForm.ThreadMessages.ImageDataLoaded, strptr.ToInt32(), 0); } // This requests loading the image protected virtual void LocalLoadImage() { lock(this) { // Bitmap loaded successfully? if(bitmap != null) { // Bitmap has incorrect format? if(bitmap.PixelFormat != PixelFormat.Format32bppArgb) { if(dynamictexture) throw new Exception("Dynamic images must be in 32 bits ARGB format."); //General.ErrorLogger.Add(ErrorType.Warning, "Image '" + name + "' does not have A8R8G8B8 pixel format. Conversion was needed."); Bitmap oldbitmap = bitmap; try { // Convert to desired pixel format bitmap = new Bitmap(oldbitmap.Size.Width, oldbitmap.Size.Height, PixelFormat.Format32bppArgb); Graphics g = Graphics.FromImage(bitmap); g.PageUnit = GraphicsUnit.Pixel; g.CompositingQuality = CompositingQuality.HighQuality; g.InterpolationMode = InterpolationMode.NearestNeighbor; g.SmoothingMode = SmoothingMode.None; g.PixelOffsetMode = PixelOffsetMode.None; g.Clear(Color.Transparent); g.DrawImage(oldbitmap, 0, 0, oldbitmap.Size.Width, oldbitmap.Size.Height); g.Dispose(); oldbitmap.Dispose(); } catch(Exception e) { bitmap = oldbitmap; General.ErrorLogger.Add(ErrorType.Warning, "Cannot lock image '" + name + "' for pixel format conversion. The image may not be displayed correctly.\n" + e.GetType().Name + ": " + e.Message); } } // This applies brightness correction on the image if(usecolorcorrection) { BitmapData bmpdata = null; try { // Try locking the bitmap bmpdata = bitmap.LockBits(new Rectangle(0, 0, bitmap.Size.Width, bitmap.Size.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); } catch(Exception e) { General.ErrorLogger.Add(ErrorType.Warning, "Cannot lock image '" + name + "' for color correction. The image may not be displayed correctly.\n" + e.GetType().Name + ": " + e.Message); } // Bitmap locked? if(bmpdata != null) { // Apply color correction PixelColor* pixels = (PixelColor*)(bmpdata.Scan0.ToPointer()); General.Colors.ApplyColorCorrection(pixels, bmpdata.Width * bmpdata.Height); bitmap.UnlockBits(bmpdata); } } } else { // Loading failed // We still mark the image as ready so that it will // not try loading again until Reload Resources is used loadfailed = true; bitmap = new Bitmap(Properties.Resources.Failed); } if(bitmap != null) { width = bitmap.Size.Width; height = bitmap.Size.Height; if(dynamictexture) { if((width != General.NextPowerOf2(width)) || (height != General.NextPowerOf2(height))) throw new Exception("Dynamic images must have a size in powers of 2."); } // Do we still have to set a scale? if((scale.x == 0.0f) && (scale.y == 0.0f)) { if((General.Map != null) && (General.Map.Config != null)) { scale.x = General.Map.Config.DefaultTextureScale; scale.y = General.Map.Config.DefaultTextureScale; } else { scale.x = 1.0f; scale.y = 1.0f; } } //mxd. Calculate average color? if(General.Map != null && General.Map.Data != null && General.Map.Data.GlowingFlats != null && General.Map.Data.GlowingFlats.ContainsKey(longname) && General.Map.Data.GlowingFlats[longname].CalculateTextureColor) { BitmapData bmpdata = null; try { bmpdata = bitmap.LockBits(new Rectangle(0, 0, bitmap.Size.Width, bitmap.Size.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); } catch(Exception e) { General.ErrorLogger.Add(ErrorType.Error, "Cannot lock image '" + this.filepathname + "' for glow color calculation. " + e.GetType().Name + ": " + e.Message); } if(bmpdata != null) { PixelColor* pixels = (PixelColor*) (bmpdata.Scan0.ToPointer()); int numpixels = bmpdata.Width * bmpdata.Height; uint r = 0; uint g = 0; uint b = 0; for(PixelColor* cp = pixels + numpixels - 1; cp >= pixels; cp--) { r += cp->r; g += cp->g; b += cp->b; // Also check alpha if(cp->a > 0 && cp->a < 255) istranslucent = true; else if (cp->a == 0) ismasked = true; } // Update glow data int br = (int)(r / numpixels); int bg = (int)(g / numpixels); int bb = (int)(b / numpixels); int max = Math.Max(br, Math.Max(bg, bb)); // Black can't glow... if(max == 0) { General.Map.Data.GlowingFlats.Remove(longname); } else { // That's how it's done in GZDoom (and I may be totally wrong about this) br = Math.Min(255, br * 153 / max); bg = Math.Min(255, bg * 153 / max); bb = Math.Min(255, bb * 153 / max); General.Map.Data.GlowingFlats[longname].Color = new PixelColor(255, (byte)br, (byte)bg, (byte)bb); General.Map.Data.GlowingFlats[longname].CalculateTextureColor = false; if(!General.Map.Data.GlowingFlats[longname].Fullbright) General.Map.Data.GlowingFlats[longname].Brightness = (br + bg + bb) / 3; } // Release the data bitmap.UnlockBits(bmpdata); } } //mxd. Check if the texture is translucent else if (!loadfailed) { BitmapData bmpdata = null; try { bmpdata = bitmap.LockBits(new Rectangle(0, 0, bitmap.Size.Width, bitmap.Size.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); } catch(Exception e) { General.ErrorLogger.Add(ErrorType.Error, "Cannot lock image '" + this.filepathname + "' for translucency check. " + e.GetType().Name + ": " + e.Message); } if(bmpdata != null) { PixelColor* pixels = (PixelColor*)(bmpdata.Scan0.ToPointer()); int numpixels = bmpdata.Width * bmpdata.Height; for(PixelColor* cp = pixels + numpixels - 1; cp >= pixels; cp--) { // Check alpha if (cp->a > 0 && cp->a < 255) istranslucent = true; else if (cp->a == 0) ismasked = true; } // Release the data bitmap.UnlockBits(bmpdata); } } } // Image is ready imagestate = ImageLoadState.Ready; } } // This creates the Direct3D texture public virtual void CreateTexture() { lock(this) { // Only do this when texture is not created yet if(((texture == null) || (texture.Disposed)) && this.IsImageLoaded && !loadfailed) { Image img = bitmap; if(loadfailed) img = Properties.Resources.Failed; // Write to memory stream and read from memory MemoryStream memstream = new MemoryStream((img.Size.Width * img.Size.Height * 4) + 4096); img.Save(memstream, ImageFormat.Bmp); memstream.Seek(0, SeekOrigin.Begin); if(dynamictexture) { texture = Texture.FromStream(General.Map.Graphics.Device, memstream, (int)memstream.Length, img.Size.Width, img.Size.Height, mipmaplevels, Usage.Dynamic, Format.A8R8G8B8, Pool.Default, General.Map.Graphics.PostFilter, General.Map.Graphics.MipGenerateFilter, 0); } else { texture = Texture.FromStream(General.Map.Graphics.Device, memstream, (int)memstream.Length, img.Size.Width, img.Size.Height, mipmaplevels, Usage.None, Format.Unknown, Pool.Managed, General.Map.Graphics.PostFilter, General.Map.Graphics.MipGenerateFilter, 0); } memstream.Dispose(); if(dynamictexture) { if((width != texture.GetLevelDescription(0).Width) || (height != texture.GetLevelDescription(0).Height)) throw new Exception("Could not create a texture with the same size as the image."); } #if DEBUG texture.Tag = name; //mxd. Helps with tracking undisposed resources... #endif } } } // This updates a dynamic texture public void UpdateTexture() { if(!dynamictexture) throw new Exception("The image must be a dynamic image to support direct updating."); lock(this) { if((texture != null) && !texture.Disposed) { // Lock the bitmap and texture BitmapData bmpdata = bitmap.LockBits(new Rectangle(0, 0, bitmap.Size.Width, bitmap.Size.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); DataRectangle texdata = texture.LockRectangle(0, LockFlags.Discard); // Copy data int* bp = (int*)bmpdata.Scan0.ToPointer(); int* tp = (int*)texdata.Data.DataPointer.ToPointer(); for(int y = 0; y < bmpdata.Height; y++) { for(int x = 0; x < bmpdata.Width; x++) { *tp = *bp; bp++; tp++; } // Skip extra data in texture int extrapitch = (texdata.Pitch >> 2) - bmpdata.Width; tp += extrapitch; } // Unlock texture.UnlockRectangle(0); bitmap.UnlockBits(bmpdata); } } } // This destroys the Direct3D texture public void ReleaseTexture() { lock(this) { // Trash it if(texture != null) texture.Dispose(); texture = null; } } // This draws a preview public virtual void DrawPreview(Graphics target, Point targetpos) { lock(this) { // Preview ready? if(!loadfailed && (previewstate == ImageLoadState.Ready)) { // Draw preview General.Map.Data.Previews.DrawPreview(previewindex, target, targetpos); } // Loading failed? else if(loadfailed) { // Draw error bitmap targetpos = new Point(targetpos.X + ((General.Map.Data.Previews.MaxImageWidth - Properties.Resources.Hourglass.Width) >> 1), targetpos.Y + ((General.Map.Data.Previews.MaxImageHeight - Properties.Resources.Hourglass.Height) >> 1)); target.DrawImageUnscaled(Properties.Resources.Failed, targetpos); } else { // Draw loading bitmap targetpos = new Point(targetpos.X + ((General.Map.Data.Previews.MaxImageWidth - Properties.Resources.Hourglass.Width) >> 1), targetpos.Y + ((General.Map.Data.Previews.MaxImageHeight - Properties.Resources.Hourglass.Height) >> 1)); target.DrawImageUnscaled(Properties.Resources.Hourglass, targetpos); } } } // This returns a preview image public virtual Image GetPreview() { //lock(this) //{ // Preview ready? if(previewstate == ImageLoadState.Ready) { // Make a copy return General.Map.Data.Previews.GetPreviewCopy(previewindex); } // Loading failed? if(loadfailed) { // Return error bitmap return Properties.Resources.Failed; } // Return loading bitmap return Properties.Resources.Hourglass; //} } //mxd. This greatly speeds up Dictionary lookups public override int GetHashCode() { return hashcode; } #endregion } }