ソースを参照

Optimized the Sandbox example using dirty rectangles for even higher frame-rates.

David Piuva 5 年 前
コミット
8cefa264a0

+ 11 - 0
Source/DFPSR/math/IRect.h

@@ -60,6 +60,14 @@ public:
 		int32_t bottomSide = std::min(a.bottom(), b.bottom());
 		return IRect(leftSide, topSide, rightSide - leftSide, bottomSide - topSide);
 	}
+	// Returns a bounding box of the union
+	static IRect merge(const IRect &a, const IRect &b) {
+		int32_t leftSide = std::min(a.left(), b.left());
+		int32_t topSide = std::min(a.top(), b.top());
+		int32_t rightSide = std::max(a.right(), b.right());
+		int32_t bottomSide = std::max(a.bottom(), b.bottom());
+		return IRect(leftSide, topSide, rightSide - leftSide, bottomSide - topSide);
+	}
 	// Returns true iff the rectangles have an overlapping area
 	// Equivalent to hasArea(a * b)
 	static bool overlaps(const IRect& a, const IRect& b) {
@@ -86,6 +94,9 @@ public:
 inline IRect operator+(const IRect &old, const IVector2D &offset) {
 	return IRect(old.left() + offset.x, old.top() + offset.y, old.width(), old.height());
 }
+inline IRect operator-(const IRect &old, const IVector2D &offset) {
+	return IRect(old.left() - offset.x, old.top() - offset.y, old.width(), old.height());
+}
 
 // Scale everything around origin
 inline IRect operator*(const IRect &old, int32_t scalar) {

+ 61 - 0
Source/SDK/sandbox/sprite/DirtyRectangles.h

@@ -0,0 +1,61 @@
+
+#ifndef DFPSR_DIRTY_RECTANGLES
+#define DFPSR_DIRTY_RECTANGLES
+
+#include "../../../DFPSR/includeFramework.h"
+
+namespace dsr {
+
+class DirtyRectangles {
+private:
+	int32_t width = 0;
+	int32_t height = 0;
+	List<IRect> dirtyRectangles;
+public:
+	DirtyRectangles() {}
+	// Call before rendering to let the dirty rectangles know if the resolution changed
+	void setTargetResolution(int32_t width, int32_t height) {
+		if (this->width != width || this->height != height) {
+			this->width = width;
+			this->height = height;
+			this->allDirty();
+		}
+	}
+public:
+	IRect getTargetBound() const {
+		return IRect(0, 0, this->width, this->height);
+	}
+	// Call when everything needs an update
+	void allDirty() {
+		this->dirtyRectangles.clear();
+		this->dirtyRectangles.push(getTargetBound());
+	}
+	void noneDirty() {
+		this->dirtyRectangles.clear();
+	}
+	void makeRegionDirty(IRect newRegion) {
+		newRegion = IRect::cut(newRegion, getTargetBound());
+		if (newRegion.hasArea()) {
+			for (int i = 0; i < this->dirtyRectangles.length(); i++) {
+				if (IRect::touches(this->dirtyRectangles[i], newRegion)) {
+					// Merge with any existing bound
+					newRegion = IRect::merge(newRegion, this->dirtyRectangles[i]);
+					this->dirtyRectangles.remove(i);
+					// Restart the search for overlaps with a larger region
+					i = -1;
+				}
+			}
+			this->dirtyRectangles.push(newRegion);
+		}
+	}
+	int64_t getRectangleCount() const {
+		return dirtyRectangles.length();
+	}
+	IRect getRectangle(int64_t index) const {
+		return this->dirtyRectangles[index];
+	}
+};
+
+}
+
+#endif

+ 43 - 7
Source/SDK/sandbox/sprite/spriteAPI.cpp

@@ -1,8 +1,12 @@
 
 #include "spriteAPI.h"
 #include "Octree.h"
+#include "DirtyRectangles.h"
 #include "../../../DFPSR/render/ITriangle2D.h"
 
+// Comment out a flag to disable an optimization when debugging
+#define DIRTY_RECTANGLE_OPTIMIZATION
+
 namespace dsr {
 
 struct SpriteConfig {
@@ -207,7 +211,8 @@ static int getSpriteFrameIndex(const Sprite& sprite, OrthoView view) {
 	return types[sprite.typeIndex].getFrameIndex(view.worldDirection + sprite.direction);
 }
 
-void drawSprite(const Sprite& sprite, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
+// Returns a 2D bounding box of affected target pixels
+IRect drawSprite(const Sprite& sprite, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
 	int frameIndex = getSpriteFrameIndex(sprite, ortho);
 	const SpriteFrame* frame = &types[sprite.typeIndex].frames[frameIndex];
 	IVector2D screenSpace = ortho.miniTilePositionToScreenPixel(sprite.location, worldCenter) - frame->centerPoint;
@@ -225,6 +230,7 @@ void drawSprite(const Sprite& sprite, const OrthoView& ortho, const IVector2D& w
 			draw_higher(targetHeight, frame->heightImage, screenSpace.x, screenSpace.y, heightOffset);
 		}
 	}
+	return IRect(screenSpace.x, screenSpace.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
 }
 
 // The camera transform for each direction
@@ -324,6 +330,7 @@ public:
 	}
 };
 
+// BlockState keeps track of when the background itself needs to update from static objects being created or destroyed
 enum class BlockState {
 	Unused,
 	Ready,
@@ -419,7 +426,7 @@ public:
 		this->draw(sprites, ortho);
 		this->state = BlockState::Ready;
 	}
-	void draw(OrderedImageRgbaU8& diffuseTarget, OrderedImageRgbaU8& normalTarget, AlignedImageF32& heightTarget, const IRect& seenRegion) const {
+	void draw(ImageRgbaU8& diffuseTarget, ImageRgbaU8& normalTarget, ImageF32& heightTarget, const IRect& seenRegion) const {
 		if (this->state != BlockState::Unused) {
 			int left = this->worldRegion.left() - seenRegion.left();
 			int top = this->worldRegion.top() - seenRegion.top();
@@ -459,6 +466,8 @@ public:
 	// Passive background
 	// TODO: How can split-screen use multiple cameras without duplicate blocks or deleting the other camera's blocks by distance?
 	List<BackgroundBlock> backgroundBlocks;
+	// These dirty rectangles keep track of when the background has to be redrawn to the screen after having drawn a dynamic sprite, moved the camera or changed static geometry
+	DirtyRectangles dirtyBackground;
 private:
 	// Reused buffers
 	int shadowResolution;
@@ -544,15 +553,34 @@ public:
 		assert(image_getWidth(diffuseTarget) == seenRegion.width() && image_getHeight(diffuseTarget) == seenRegion.height());
 		assert(image_getWidth(normalTarget) == seenRegion.width() && image_getHeight(normalTarget) == seenRegion.height());
 		assert(image_getWidth(heightTarget) == seenRegion.width() && image_getHeight(heightTarget) == seenRegion.height());
+		this->dirtyBackground.setTargetResolution(seenRegion.width(), seenRegion.height());
 		// Draw passive sprites to blocks
 		this->updateBlocks(seenRegion);
-		// Draw blocks to the targets
+
+		// Draw background blocks to the target images
 		for (int b = 0; b < this->backgroundBlocks.length(); b++) {
-			this->backgroundBlocks[b].draw(diffuseTarget, normalTarget, heightTarget, seenRegion);
+			#ifdef DIRTY_RECTANGLE_OPTIMIZATION
+				// Optimized version
+				for (int64_t r = 0; r < this->dirtyBackground.getRectangleCount(); r++) {
+					IRect screenClip = this->dirtyBackground.getRectangle(r);
+					IRect worldClip = screenClip + seenRegion.upperLeft();
+					ImageRgbaU8 clippedDiffuseTarget = image_getSubImage(diffuseTarget, screenClip);
+					ImageRgbaU8 clippedNormalTarget = image_getSubImage(normalTarget, screenClip);
+					ImageF32 clippedHeightTarget = image_getSubImage(heightTarget, screenClip);
+					this->backgroundBlocks[b].draw(clippedDiffuseTarget, clippedNormalTarget, clippedHeightTarget, worldClip);
+				}
+			#else
+				// Reference implementation
+				this->backgroundBlocks[b].draw(diffuseTarget, normalTarget, heightTarget, seenRegion);
+			#endif
 		}
+
+		// Reset dirty rectangles so that active sprites may record changes
+		this->dirtyBackground.noneDirty();
 		// Draw active sprites to the targets
 		for (int s = 0; s < this->temporarySprites.length(); s++) {
-			drawSprite(this->temporarySprites[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
+			IRect drawnRegion = drawSprite(this->temporarySprites[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
+			this->dirtyBackground.makeRegionDirty(drawnRegion);
 		}
 	}
 public:
@@ -567,6 +595,8 @@ public:
 				this->invalidateBlockAt(x, y);
 			}
 		}
+		// Redrawing the whole background to the screen is very cheap using memcpy, so no need to optimize this rare event
+		this->dirtyBackground.allDirty();
 	}
 	IVector2D findWorldCenter(const AlignedImageRgbaU8& colorTarget) const {
 		return IVector2D(image_getWidth(colorTarget) / 2, image_getHeight(colorTarget) / 2) - this->ortho.miniTileOffsetToScreenPixel(this->cameraLocation, this->cameraIndex);
@@ -696,7 +726,10 @@ IVector3D spriteWorld_findGroundAtPixel(SpriteWorld& world, const AlignedImageRg
 
 void spriteWorld_moveCameraInPixels(SpriteWorld& world, const IVector2D& pixelOffset) {
 	MUST_EXIST(world, spriteWorld_moveCameraInPixels);
-	world->cameraLocation = world->cameraLocation + world->ortho.pixelToMiniOffset(pixelOffset, world->cameraIndex);
+	if (pixelOffset.x != 0 && pixelOffset.y != 0) {
+		world->cameraLocation = world->cameraLocation + world->ortho.pixelToMiniOffset(pixelOffset, world->cameraIndex);
+		world->dirtyBackground.allDirty();
+	}
 }
 
 AlignedImageRgbaU8 spriteWorld_getDiffuseBuffer(SpriteWorld& world) {
@@ -726,7 +759,10 @@ int spriteWorld_getCameraDirectionIndex(SpriteWorld& world) {
 
 void spriteWorld_setCameraDirectionIndex(SpriteWorld& world, int index) {
 	MUST_EXIST(world, spriteWorld_setCameraDirectionIndex);
-	world->cameraIndex = index;
+	if (index != world->cameraIndex) {
+		world->cameraIndex = index;
+		world->dirtyBackground.allDirty();
+	}
 }
 
 static FVector3D normalFromPoints(const FVector3D& A, const FVector3D& B, const FVector3D& C) {