#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.Collections.Generic; using System.Drawing; using System.IO; using CodeImp.DoomBuilder.Data; using CodeImp.DoomBuilder.Map; using SlimDX; using SlimDX.Direct3D9; #endregion namespace CodeImp.DoomBuilder.Rendering { internal class SurfaceManager : ID3DResource { #region ================== Constants // The true maximum lies at 65535 if I remember correctly, but that // is a scary big number for a vertexbuffer. private const int MAX_VERTICES_PER_BUFFER = 30000; // When a sector exceeds this number of vertices, it should split up it's triangles // This number must be a multiple of 3. public const int MAX_VERTICES_PER_SECTOR = 6000; #endregion #region ================== Variables // Set of buffers for a specific number of vertices per sector private Dictionary sets; // List of buffers that are locked // This is null when not in the process of updating private List lockedbuffers; // Surface to be rendered. // Each BinaryHeap in the Dictionary contains all geometry that needs // to be rendered with the associated ImageData. // The BinaryHeap sorts the geometry by sector to minimize stream switchs. // This is null when not in the process of rendering private Dictionary> surfaces; // This is 1 to add the number of vertices to the offset // (effectively rendering the ceiling vertices instead of floor vertices) private int surfacevertexoffsetmul; // This is set to true when the resources have been unloaded private bool resourcesunloaded; #endregion #region ================== Properties #endregion #region ================== Constructor / Disposer // Constructor public SurfaceManager() { sets = new Dictionary(); lockedbuffers = new List(); General.Map.Graphics.RegisterResource(this); } // Disposer public void Dispose() { if(sets != null) { General.Map.Graphics.UnregisterResource(this); // Dispose all sets foreach(KeyValuePair set in sets) { // Dispose vertex buffers for(int i = 0; i < set.Value.buffers.Count; i++) { if(set.Value.buffers[i] != null) { set.Value.buffers[i].Dispose(); set.Value.buffers[i] = null; } } } sets = null; } } #endregion #region ================== Management // Called when all resource must be unloaded public void UnloadResource() { resourcesunloaded = true; foreach(KeyValuePair set in sets) { // Dispose vertex buffers for(int i = 0; i < set.Value.buffers.Count; i++) { if(set.Value.buffers[i] != null) { set.Value.buffers[i].Dispose(); set.Value.buffers[i] = null; } } } lockedbuffers.Clear(); } // Called when all resource must be reloaded public void ReloadResource() { foreach(KeyValuePair set in sets) { // Rebuild vertex buffers for(int i = 0; i < set.Value.buffersizes.Count; i++) { // Make the new buffer! VertexBuffer b = new VertexBuffer(General.Map.Graphics.Device, FlatVertex.Stride * set.Value.buffersizes[i], Usage.WriteOnly | Usage.Dynamic, VertexFormat.None, Pool.Default); // Start refilling the buffer with sector geometry DataStream bstream = b.Lock(0, FlatVertex.Stride * set.Value.buffersizes[i], LockFlags.Discard); foreach(SurfaceEntry e in set.Value.entries) { if(e.bufferindex == i) { // Fill buffer bstream.Seek(e.vertexoffset * FlatVertex.Stride, SeekOrigin.Begin); bstream.WriteRange(e.floorvertices); bstream.WriteRange(e.ceilvertices); } } // Unlock buffer b.Unlock(); bstream.Dispose(); // Add to list set.Value.buffers[i] = b; } } resourcesunloaded = false; } // This resets all buffers and requires all sectors to get new entries public void Reset() { // Clear all items foreach(KeyValuePair set in sets) { foreach(SurfaceEntry entry in set.Value.entries) { entry.numvertices = -1; entry.bufferindex = -1; } foreach(SurfaceEntry entry in set.Value.holes) { entry.numvertices = -1; entry.bufferindex = -1; } foreach(VertexBuffer vb in set.Value.buffers) vb.Dispose(); } // New dictionary sets = new Dictionary(); } // Updating sector surface geometry should go in this order; // - Triangulate sectors // - Call FreeSurfaces to remove entries that have changed number of vertices // - Call AllocateBuffers // - Call UpdateSurfaces to add/update entries // - Call UnlockBuffers // This (re)allocates the buffers based on an analysis of the map // The map must be updated (triangulated) before calling this public void AllocateBuffers() { // Make analysis of sector geometry Dictionary sectorverts = new Dictionary(); foreach(Sector s in General.Map.Map.Sectors) { if(s.Triangles != null) { int numvertices = s.Triangles.Vertices.Count; while(numvertices > 0) { // Determine for how many vertices in this entry int vertsinentry = (numvertices > MAX_VERTICES_PER_SECTOR) ? MAX_VERTICES_PER_SECTOR : numvertices; // We count the number of sectors that have specific number of vertices if(!sectorverts.ContainsKey(vertsinentry)) sectorverts.Add(vertsinentry, 0); sectorverts[vertsinentry]++; numvertices -= vertsinentry; } } } // Now (re)allocate the needed buffers foreach(KeyValuePair sv in sectorverts) { // Zero vertices can't be drawn if(sv.Key > 0) { SurfaceBufferSet set = GetSet(sv.Key); // Calculte how many free entries we need int neededentries = sv.Value; int freeentriesneeded = neededentries - set.entries.Count; // Allocate the space needed EnsureFreeBufferSpace(set, freeentriesneeded); } } } // This ensures there is enough space for a given number of free entries (also adds new bufers if needed) private void EnsureFreeBufferSpace(SurfaceBufferSet set, int freeentries) { DataStream bstream = null; VertexBuffer vb = null; // Check if we have to add entries int addentries = freeentries - set.holes.Count; // Begin resizing buffers starting with the last in this set int bufferindex = set.buffers.Count - 1; // Calculate the maximum number of entries we can put in a new buffer // Note that verticesperentry is the number of vertices multiplied by 2, because // we have to store both the floor and ceiling int verticesperentry = set.numvertices * 2; int maxentriesperbuffer = MAX_VERTICES_PER_BUFFER / verticesperentry; // Make a new bufer when the last one is full if((bufferindex > -1) && (set.buffersizes[bufferindex] >= (maxentriesperbuffer * verticesperentry))) bufferindex = -1; while(addentries > 0) { // Create a new buffer? if((bufferindex == -1) || (bufferindex > (set.buffers.Count - 1))) { // Determine the number of entries we will be making this buffer for int bufferentries = (addentries > maxentriesperbuffer) ? maxentriesperbuffer : addentries; // Calculate the number of vertices that will be int buffernumvertices = bufferentries * verticesperentry; if(!resourcesunloaded) { // Make the new buffer! vb = new VertexBuffer(General.Map.Graphics.Device, FlatVertex.Stride * buffernumvertices, Usage.WriteOnly | Usage.Dynamic, VertexFormat.None, Pool.Default); // Add it. set.buffers.Add(vb); } else { // We can't make a vertexbuffer right now set.buffers.Add(null); } // Also add available entries as holes, because they are not used yet. set.buffersizes.Add(buffernumvertices); for(int i = 0; i < bufferentries; i++) set.holes.Add(new SurfaceEntry(set.numvertices, set.buffers.Count - 1, i * verticesperentry)); // Done addentries -= bufferentries; } // Reallocate a buffer else { // Trash the old buffer if(set.buffers[bufferindex].Tag != null) { bstream = (DataStream)set.buffers[bufferindex].Tag; set.buffers[bufferindex].Unlock(); bstream.Dispose(); set.buffers[bufferindex].Tag = null; } if((set.buffers[bufferindex] != null) && !resourcesunloaded) set.buffers[bufferindex].Dispose(); // Get the entries that are in this buffer only List theseentries = new List(); foreach(SurfaceEntry e in set.entries) { if(e.bufferindex == bufferindex) theseentries.Add(e); } // Determine the number of entries we will be making this buffer for int bufferentries = ((theseentries.Count + addentries) > maxentriesperbuffer) ? maxentriesperbuffer : (theseentries.Count + addentries); // Calculate the number of vertices that will be int buffernumvertices = bufferentries * verticesperentry; if(!resourcesunloaded) { // Make the new buffer and lock it vb = new VertexBuffer(General.Map.Graphics.Device, FlatVertex.Stride * buffernumvertices, Usage.WriteOnly | Usage.Dynamic, VertexFormat.None, Pool.Default); bstream = vb.Lock(0, FlatVertex.Stride * theseentries.Count * verticesperentry, LockFlags.Discard); } // Start refilling the buffer with sector geometry int vertexoffset = 0; foreach(SurfaceEntry e in theseentries) { if(!resourcesunloaded) { // Fill buffer bstream.WriteRange(e.floorvertices); bstream.WriteRange(e.ceilvertices); } // Set the new location in the buffer e.vertexoffset = vertexoffset; // Move on vertexoffset += verticesperentry; } if(!resourcesunloaded) { // Unlock buffer vb.Unlock(); bstream.Dispose(); set.buffers[bufferindex] = vb; } else { // No vertex buffer at this time, sorry set.buffers[bufferindex] = null; } // Set the new buffer and add available entries as holes, because they are not used yet. set.buffersizes[bufferindex] = buffernumvertices; set.holes.Clear(); for(int i = 0; i < bufferentries - theseentries.Count; i++) set.holes.Add(new SurfaceEntry(set.numvertices, bufferindex, i * verticesperentry + vertexoffset)); // Done addentries -= bufferentries; } // Always continue in next (new) buffer bufferindex = set.buffers.Count; } } // This adds or updates sector geometry into a buffer. // Modiies the list of SurfaceEntries with the new surface entry for the stored geometry. public void UpdateSurfaces(SurfaceEntryCollection entries, SurfaceUpdate update) { // Free entries when number of vertices has changed if((entries.Count > 0) && (entries.totalvertices != update.numvertices)) { FreeSurfaces(entries); entries.Clear(); } if((entries.Count == 0) && (update.numvertices > 0)) { #if DEBUG if((update.floorvertices == null) || (update.ceilvertices == null)) General.Fail("We need both floor and ceiling vertices when the number of vertices changes!"); #endif // If we have no entries yet, we have to make them now int vertsremaining = update.numvertices; while(vertsremaining > 0) { // Determine for how many vertices in this entry int vertsinentry = (vertsremaining > MAX_VERTICES_PER_SECTOR) ? MAX_VERTICES_PER_SECTOR : vertsremaining; // Lookup the set that holds entries for this number of vertices SurfaceBufferSet set = GetSet(vertsinentry); // Make sure we can get a new entry in this set EnsureFreeBufferSpace(set, 1); // Get a new entry in this set SurfaceEntry e = set.holes[set.holes.Count - 1]; set.holes.RemoveAt(set.holes.Count - 1); set.entries.Add(e); // Fill the entry data e.floorvertices = new FlatVertex[vertsinentry]; e.ceilvertices = new FlatVertex[vertsinentry]; Array.Copy(update.floorvertices, update.numvertices - vertsremaining, e.floorvertices, 0, vertsinentry); Array.Copy(update.ceilvertices, update.numvertices - vertsremaining, e.ceilvertices, 0, vertsinentry); e.floortexture = update.floortexture; e.ceiltexture = update.ceiltexture; entries.Add(e); vertsremaining -= vertsinentry; } } else { // We re-use the same entries, just copy over the updated data int vertsremaining = update.numvertices; foreach(SurfaceEntry e in entries) { if(update.floorvertices != null) { Array.Copy(update.floorvertices, update.numvertices - vertsremaining, e.floorvertices, 0, e.numvertices); e.floortexture = update.floortexture; } if(update.ceilvertices != null) { Array.Copy(update.ceilvertices, update.numvertices - vertsremaining, e.ceilvertices, 0, e.numvertices); e.ceiltexture = update.ceiltexture; } vertsremaining -= e.numvertices; } } entries.totalvertices = update.numvertices; // Time to update or create the buffers foreach(SurfaceEntry e in entries) { SurfaceBufferSet set = GetSet(e.numvertices); // Update bounding box e.UpdateBBox(); if(!resourcesunloaded) { // Lock the buffer DataStream bstream; VertexBuffer vb = set.buffers[e.bufferindex]; if(vb.Tag == null) { // Note: DirectX warns me that I am not using LockFlags.Discard or LockFlags.NoOverwrite here, // but we don't have much of a choice since we want to update our data and not destroy other data bstream = vb.Lock(0, set.buffersizes[e.bufferindex] * FlatVertex.Stride, LockFlags.None); vb.Tag = bstream; lockedbuffers.Add(vb); } else { bstream = (DataStream)vb.Tag; } // Write the vertices to buffer bstream.Seek(e.vertexoffset * FlatVertex.Stride, SeekOrigin.Begin); bstream.WriteRange(e.floorvertices); bstream.WriteRange(e.ceilvertices); } } } // This frees the given surface entry public void FreeSurfaces(SurfaceEntryCollection entries) { foreach(SurfaceEntry e in entries) { if((e.numvertices > 0) && (e.bufferindex > -1)) { SurfaceBufferSet set = sets[e.numvertices]; set.entries.Remove(e); SurfaceEntry newentry = new SurfaceEntry(e); set.holes.Add(newentry); } e.numvertices = -1; e.bufferindex = -1; } } // This unlocks the locked buffers public void UnlockBuffers() { if(!resourcesunloaded) { foreach(VertexBuffer vb in lockedbuffers) { if(vb.Tag != null) { DataStream bstream = (DataStream)vb.Tag; vb.Unlock(); bstream.Dispose(); vb.Tag = null; } } // Clear list lockedbuffers = new List(); } } // This gets or creates a set for a specific number of vertices private SurfaceBufferSet GetSet(int numvertices) { SurfaceBufferSet set; // Get or create the set if(!sets.ContainsKey(numvertices)) { set = new SurfaceBufferSet(); set.numvertices = numvertices; set.buffers = new List(); set.buffersizes = new List(); set.entries = new List(); set.holes = new List(); sets.Add(numvertices, set); } else { set = sets[numvertices]; } return set; } #endregion #region ================== Rendering // This renders all sector floors internal void RenderSectorFloors(RectangleF viewport) { surfaces = new Dictionary>(); surfacevertexoffsetmul = 0; // Go for all surfaces as they are sorted in the buffers, so that // they are automatically already sorted by vertexbuffer foreach(KeyValuePair set in sets) { foreach(SurfaceEntry entry in set.Value.entries) { if(entry.bbox.IntersectsWith(viewport)) AddSurfaceEntryForRendering(entry, entry.floortexture); } } } // This renders all sector ceilings internal void RenderSectorCeilings(RectangleF viewport) { surfaces = new Dictionary>(); surfacevertexoffsetmul = 1; // Go for all surfaces as they are sorted in the buffers, so that // they are automatically already sorted by vertexbuffer foreach(KeyValuePair set in sets) { foreach(SurfaceEntry entry in set.Value.entries) { if(entry.bbox.IntersectsWith(viewport)) AddSurfaceEntryForRendering(entry, entry.ceiltexture); } } } // This renders all sector brightness levels internal void RenderSectorBrightness(RectangleF viewport) { surfaces = new Dictionary>(); surfacevertexoffsetmul = 0; // Go for all surfaces as they are sorted in the buffers, so that // they are automatically already sorted by vertexbuffer foreach(KeyValuePair set in sets) { foreach(SurfaceEntry entry in set.Value.entries) { if(entry.bbox.IntersectsWith(viewport)) AddSurfaceEntryForRendering(entry, 0); } } } // This adds a surface entry to the list of surfaces private void AddSurfaceEntryForRendering(SurfaceEntry entry, long longimagename) { // Determine texture to use ImageData img; if(longimagename == 0) { img = General.Map.Data.WhiteTexture; } else { if(longimagename == MapSet.EmptyLongName) { img = General.Map.Data.MissingTexture3D; } else { img = General.Map.Data.GetFlatImage(longimagename); if(img is UnknownImage) { img = General.Map.Data.UnknownTexture3D; } else { if(img.IsImageLoaded && !img.LoadFailed) { if(img.Texture == null) img.CreateTexture(); } else { img = General.Map.Data.WhiteTexture; } } } } // Store by texture if(!surfaces.ContainsKey(img)) surfaces.Add(img, new List()); surfaces[img].Add(entry); } // This renders the sorted sector surfaces internal void RenderSectorSurfaces(D3DDevice graphics) { if(!resourcesunloaded) { int pass = Renderer.FullBrightness ? 2 : 1; //mxd graphics.Shaders.Display2D.Begin(); foreach(KeyValuePair> imgsurfaces in surfaces) { // Set texture graphics.Shaders.Display2D.Texture1 = imgsurfaces.Key.Texture; graphics.Shaders.Display2D.BeginPass(pass); // Go for all surfaces VertexBuffer lastbuffer = null; foreach(SurfaceEntry entry in imgsurfaces.Value) { // Set the vertex buffer SurfaceBufferSet set = sets[entry.numvertices]; if(set.buffers[entry.bufferindex] != lastbuffer) { lastbuffer = set.buffers[entry.bufferindex]; graphics.Device.SetStreamSource(0, lastbuffer, 0, FlatVertex.Stride); } // Draw graphics.Device.DrawPrimitives(PrimitiveType.TriangleList, entry.vertexoffset + (entry.numvertices * surfacevertexoffsetmul), entry.numvertices / 3); } graphics.Shaders.Display2D.EndPass(); } graphics.Shaders.Display2D.End(); } } #endregion } }