Просмотр исходного кода

Introduce BatchDrawState for h2d (#844)

Pavel Alexandrov 5 лет назад
Родитель
Сommit
8b5af9c718
6 измененных файлов с 427 добавлено и 37 удалено
  1. 29 23
      h2d/Graphics.hx
  2. 22 0
      h2d/RenderContext.hx
  3. 18 6
      h2d/SpriteBatch.hx
  4. 41 8
      h2d/TileGroup.hx
  5. 205 0
      h2d/impl/BatchDrawState.hx
  6. 112 0
      samples/DrawingTiles.hx

+ 29 - 23
h2d/Graphics.hx

@@ -1,4 +1,6 @@
 package h2d;
 package h2d;
+import h2d.RenderContext;
+import h2d.impl.BatchDrawState;
 import hxd.Math;
 import hxd.Math;
 
 
 private typedef GraphicsPoint = hxd.poly2tri.Point;
 private typedef GraphicsPoint = hxd.poly2tri.Point;
@@ -28,8 +30,9 @@ private class GraphicsContent extends h3d.prim.Primitive {
 
 
 	var tmp : hxd.FloatBuffer;
 	var tmp : hxd.FloatBuffer;
 	var index : hxd.IndexBuffer;
 	var index : hxd.IndexBuffer;
+	var state : BatchDrawState;
 
 
-	var buffers : Array<{ buf : hxd.FloatBuffer, vbuf : h3d.Buffer, idx : hxd.IndexBuffer, ibuf : h3d.Indexes }>;
+	var buffers : Array<{ buf : hxd.FloatBuffer, vbuf : h3d.Buffer, idx : hxd.IndexBuffer, ibuf : h3d.Indexes, state : BatchDrawState }>;
 	var bufferDirty : Bool;
 	var bufferDirty : Bool;
 	var indexDirty : Bool;
 	var indexDirty : Bool;
 	#if track_alloc
 	#if track_alloc
@@ -38,6 +41,7 @@ private class GraphicsContent extends h3d.prim.Primitive {
 
 
 	public function new() {
 	public function new() {
 		buffers = [];
 		buffers = [];
+		state = new BatchDrawState();
 		#if track_alloc
 		#if track_alloc
 		this.allocPos = new hxd.impl.AllocPos();
 		this.allocPos = new hxd.impl.AllocPos();
 		#end
 		#end
@@ -45,6 +49,7 @@ private class GraphicsContent extends h3d.prim.Primitive {
 
 
 	public inline function addIndex(i) {
 	public inline function addIndex(i) {
 		index.push(i);
 		index.push(i);
+		state.add(1);
 		indexDirty = true;
 		indexDirty = true;
 	}
 	}
 
 
@@ -60,13 +65,22 @@ private class GraphicsContent extends h3d.prim.Primitive {
 		bufferDirty = true;
 		bufferDirty = true;
 	}
 	}
 
 
+	public function setTile( tile : h2d.Tile ) {
+		state.setTile(tile);
+	}
+
 	public function next() {
 	public function next() {
 		var nvect = tmp.length >> 3;
 		var nvect = tmp.length >> 3;
-		if( nvect < 1 << 15 )
+		if( nvect < 1 << 15 ) {
 			return false;
 			return false;
-		buffers.push( { buf : tmp, idx : index, vbuf : null, ibuf : null } );
+		}
+		buffers.push( { buf : tmp, idx : index, vbuf : null, ibuf : null, state: state } );
+		
 		tmp = new hxd.FloatBuffer();
 		tmp = new hxd.FloatBuffer();
 		index = new hxd.IndexBuffer();
 		index = new hxd.IndexBuffer();
+		var tex = state.currentTexture;
+		state = new BatchDrawState();
+		state.setTexture(tex);
 		super.dispose();
 		super.dispose();
 		return true;
 		return true;
 	}
 	}
@@ -86,12 +100,11 @@ private class GraphicsContent extends h3d.prim.Primitive {
 		indexDirty = false;
 		indexDirty = false;
 	}
 	}
 
 
-	override function render( engine : h3d.Engine ) {
-		if (index.length <= 0) return ;
+	public function doRender( ctx : h2d.RenderContext ) {
+		if (index.length <= 0) return;
 		flush();
 		flush();
-		for( b in buffers )
-			engine.renderIndexed(b.vbuf, b.ibuf);
-		super.render(engine);
+		for ( b in buffers ) b.state.drawIndexed(ctx, b.vbuf, b.ibuf);
+		state.drawIndexed(ctx, buffer, indexes);
 	}
 	}
 
 
 	public inline function flush() {
 	public inline function flush() {
@@ -115,9 +128,11 @@ private class GraphicsContent extends h3d.prim.Primitive {
 		for( b in buffers ) {
 		for( b in buffers ) {
 			if( b.vbuf != null ) b.vbuf.dispose();
 			if( b.vbuf != null ) b.vbuf.dispose();
 			if( b.ibuf != null ) b.ibuf.dispose();
 			if( b.ibuf != null ) b.ibuf.dispose();
+			b.state.clear();
 			b.vbuf = null;
 			b.vbuf = null;
 			b.ibuf = null;
 			b.ibuf = null;
 		}
 		}
+		state.clear();
 		super.dispose();
 		super.dispose();
 	}
 	}
 
 
@@ -393,22 +408,13 @@ class Graphics extends Drawable {
 		to these coordinates.
 		to these coordinates.
 	**/
 	**/
 	public function beginTileFill( ?dx : Float, ?dy : Float, ?scaleX : Float, ?scaleY : Float, ?tile : h2d.Tile ) {
 	public function beginTileFill( ?dx : Float, ?dy : Float, ?scaleX : Float, ?scaleY : Float, ?tile : h2d.Tile ) {
+		if( tile == null )
+			throw "Tile not specified";
+		this.tile = tile;
 		beginFill(0xFFFFFF);
 		beginFill(0xFFFFFF);
+		content.setTile(tile);
 		if( dx == null ) dx = 0;
 		if( dx == null ) dx = 0;
 		if( dy == null ) dy = 0;
 		if( dy == null ) dy = 0;
-		if( tile != null ) {
-			if( this.tile != null && tile.getTexture() != this.tile.getTexture() ) {
-				var tex = this.tile.getTexture();
-				if( tex.width != 1 || tex.height != 1 )
-					throw "All tiles must be of the same texture";
-				this.tile = tile;
-			}
-			if( this.tile == null  )
-				this.tile = tile;
-		} else
-			tile = this.tile;
-		if( tile == null )
-			throw "Tile not specified";
 		if( scaleX == null ) scaleX = 1;
 		if( scaleX == null ) scaleX = 1;
 		if( scaleY == null ) scaleY = 1;
 		if( scaleY == null ) scaleY = 1;
 		dx -= tile.x;
 		dx -= tile.x;
@@ -641,8 +647,8 @@ class Graphics extends Drawable {
 	}
 	}
 
 
 	override function draw(ctx:RenderContext) {
 	override function draw(ctx:RenderContext) {
-		if( !ctx.beginDrawObject(this, tile.getTexture()) ) return;
-		content.render(ctx.engine);
+		if( !ctx.beginDrawBatchState(this) ) return;
+		content.doRender(ctx);
 	}
 	}
 
 
 	override function sync(ctx:RenderContext) {
 	override function sync(ctx:RenderContext) {

+ 22 - 0
h2d/RenderContext.hx

@@ -449,6 +449,28 @@ class RenderContext extends h3d.impl.RenderContext {
 			baseShader.color.set(obj.color.r, obj.color.g, obj.color.b, obj.color.a * globalAlpha);
 			baseShader.color.set(obj.color.r, obj.color.g, obj.color.b, obj.color.a * globalAlpha);
 	}
 	}
 
 
+	// BatchState render
+	/**
+		Prepares rendering with BatchState.
+		Each state draw should be preceded with `swapTexture` call.
+	**/
+	@:access(h2d.Drawable)
+	public function beginDrawBatchState( obj : h2d.Drawable ) {
+		if ( !beginDraw(obj, null, true) ) return false;
+		setupColor(obj);
+		baseShader.absoluteMatrixA.set(obj.matA, obj.matC, obj.absX);
+		baseShader.absoluteMatrixB.set(obj.matB, obj.matD, obj.absY);
+		return true;
+	}
+
+	/**
+		Swap current active texture and prepares for next drawcall.
+	**/
+	public inline function swapTexture( texture : h3d.mat.Texture ) {
+		this.texture = texture;
+		beforeDraw();
+	}
+
 	@:access(h2d.Drawable)
 	@:access(h2d.Drawable)
 	public function beginDrawObject( obj : h2d.Drawable, texture : h3d.mat.Texture ) {
 	public function beginDrawObject( obj : h2d.Drawable, texture : h3d.mat.Texture ) {
 		if ( !beginDraw(obj, texture, true) ) return false;
 		if ( !beginDraw(obj, texture, true) ) return false;

+ 18 - 6
h2d/SpriteBatch.hx

@@ -1,5 +1,8 @@
 package h2d;
 package h2d;
 
 
+import h2d.RenderContext;
+import h2d.impl.BatchDrawState;
+
 private class ElementsIterator {
 private class ElementsIterator {
 	var e : BatchElement;
 	var e : BatchElement;
 	public inline function new(e) {
 	public inline function new(e) {
@@ -95,11 +98,13 @@ class SpriteBatch extends Drawable {
 	var last : BatchElement;
 	var last : BatchElement;
 	var tmpBuf : hxd.FloatBuffer;
 	var tmpBuf : hxd.FloatBuffer;
 	var buffer : h3d.Buffer;
 	var buffer : h3d.Buffer;
-	var bufferVertices : Int;
+	var state : BatchDrawState;
+	var empty : Bool;
 
 
 	public function new(t,?parent) {
 	public function new(t,?parent) {
 		super(parent);
 		super(parent);
 		tile = t;
 		tile = t;
+		state = new BatchDrawState();
 	}
 	}
 
 
 	public function add(e:BatchElement,before=false) {
 	public function add(e:BatchElement,before=false) {
@@ -194,14 +199,16 @@ class SpriteBatch extends Drawable {
 	}
 	}
 
 
 	function flush() {
 	function flush() {
-		if( first == null ){
-			bufferVertices = 0;
+		if( first == null ) {
 			return;
 			return;
 		}
 		}
 		if( tmpBuf == null ) tmpBuf = new hxd.FloatBuffer();
 		if( tmpBuf == null ) tmpBuf = new hxd.FloatBuffer();
 		var pos = 0;
 		var pos = 0;
 		var e = first;
 		var e = first;
 		var tmp = tmpBuf;
 		var tmp = tmpBuf;
+		var bufferVertices = 0;
+		state.clear();
+
 		while( e != null ) {
 		while( e != null ) {
 			if( !e.visible ) {
 			if( !e.visible ) {
 				e = e.next;
 				e = e.next;
@@ -209,6 +216,8 @@ class SpriteBatch extends Drawable {
 			}
 			}
 
 
 			var t = e.t;
 			var t = e.t;
+			state.setTile(t);
+			state.add(4);
 
 
 			tmp.grow(pos + 8 * 4);
 			tmp.grow(pos + 8 * 4);
 
 
@@ -298,6 +307,7 @@ class SpriteBatch extends Drawable {
 			buffer.dispose();
 			buffer.dispose();
 			buffer = null;
 			buffer = null;
 		}
 		}
+		empty = bufferVertices == 0;
 		if( bufferVertices > 0 )
 		if( bufferVertices > 0 )
 			buffer = h3d.Buffer.ofSubFloats(tmpBuf, 8, bufferVertices, [Dynamic, Quads, RawFormat]);
 			buffer = h3d.Buffer.ofSubFloats(tmpBuf, 8, bufferVertices, [Dynamic, Quads, RawFormat]);
 	}
 	}
@@ -308,9 +318,10 @@ class SpriteBatch extends Drawable {
 
 
 	@:allow(h2d)
 	@:allow(h2d)
 	function drawWith( ctx:RenderContext, obj : Drawable ) {
 	function drawWith( ctx:RenderContext, obj : Drawable ) {
-		if( first == null || buffer == null || buffer.isDisposed() || bufferVertices == 0 ) return;
-		if( !ctx.beginDrawObject(obj, tile.getTexture()) ) return;
-		ctx.engine.renderQuadBuffer(buffer, 0, bufferVertices>>1);
+		if( first == null || buffer == null || buffer.isDisposed() || empty ) return;
+		if( !ctx.beginDrawBatchState(obj) ) return;
+		var engine = ctx.engine;
+		state.drawQuads(ctx, buffer);
 	}
 	}
 
 
 	public inline function isEmpty() {
 	public inline function isEmpty() {
@@ -327,5 +338,6 @@ class SpriteBatch extends Drawable {
 			buffer.dispose();
 			buffer.dispose();
 			buffer = null;
 			buffer = null;
 		}
 		}
+		state.clear();
 	}
 	}
 }
 }

+ 41 - 8
h2d/TileGroup.hx

@@ -1,5 +1,8 @@
 package h2d;
 package h2d;
 
 
+import h2d.RenderContext;
+import h2d.impl.BatchDrawState;
+
 class TileLayerContent extends h3d.prim.Primitive {
 class TileLayerContent extends h3d.prim.Primitive {
 
 
 	var tmp : hxd.FloatBuffer;
 	var tmp : hxd.FloatBuffer;
@@ -8,7 +11,10 @@ class TileLayerContent extends h3d.prim.Primitive {
 	public var xMax : Float;
 	public var xMax : Float;
 	public var yMax : Float;
 	public var yMax : Float;
 
 
+	var state : BatchDrawState;
+
 	public function new() {
 	public function new() {
+		state = new BatchDrawState();
 		clear();
 		clear();
 	}
 	}
 
 
@@ -20,6 +26,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		yMin = hxd.Math.POSITIVE_INFINITY;
 		yMin = hxd.Math.POSITIVE_INFINITY;
 		xMax = hxd.Math.NEGATIVE_INFINITY;
 		xMax = hxd.Math.NEGATIVE_INFINITY;
 		yMax = hxd.Math.NEGATIVE_INFINITY;
 		yMax = hxd.Math.NEGATIVE_INFINITY;
+		state.clear();
 	}
 	}
 
 
 	public function isEmpty() {
 	public function isEmpty() {
@@ -77,6 +84,9 @@ class TileLayerContent extends h3d.prim.Primitive {
 		y += t.height;
 		y += t.height;
 		if( x > xMax ) xMax = x;
 		if( x > xMax ) xMax = x;
 		if( y > yMax ) yMax = y;
 		if( y > yMax ) yMax = y;
+
+		state.setTile(t);
+		state.add(4);
 	}
 	}
 
 
 	public function addTransform( x : Float, y : Float, sx : Float, sy : Float, r : Float, c : h3d.Vector, t : Tile ) {
 	public function addTransform( x : Float, y : Float, sx : Float, sy : Float, r : Float, c : h3d.Vector, t : Tile ) {
@@ -146,8 +156,15 @@ class TileLayerContent extends h3d.prim.Primitive {
 		tmp.push(c.b);
 		tmp.push(c.b);
 		tmp.push(c.a);
 		tmp.push(c.a);
 		updateBounds(px, py);
 		updateBounds(px, py);
+
+		state.setTile(t);
+		state.add(4);
 	}
 	}
 
 
+	/**
+		Usage warning: When adding geometry trough addPoint, they should be added in groups of 4 that form a quad,
+		and then `updateState(null, quads * 2)` should be called to ensure proper batch rendering.
+	**/
 	public function addPoint( x : Float, y : Float, color : Int ) {
 	public function addPoint( x : Float, y : Float, color : Int ) {
 		tmp.push(x);
 		tmp.push(x);
 		tmp.push(y);
 		tmp.push(y);
@@ -195,6 +212,8 @@ class TileLayerContent extends h3d.prim.Primitive {
 		y += h;
 		y += h;
 		if( x > xMax ) xMax = x;
 		if( x > xMax ) xMax = x;
 		if( y > yMax ) yMax = y;
 		if( y > yMax ) yMax = y;
+
+		state.add(4);
 	}
 	}
 
 
 	public inline function rectGradient( x : Float, y : Float, w : Float, h : Float, ctl : Int, ctr : Int, cbl : Int, cbr : Int ) {
 	public inline function rectGradient( x : Float, y : Float, w : Float, h : Float, ctl : Int, ctr : Int, cbl : Int, cbr : Int ) {
@@ -225,6 +244,8 @@ class TileLayerContent extends h3d.prim.Primitive {
 		y += h;
 		y += h;
 		if( x > xMax ) xMax = x;
 		if( x > xMax ) xMax = x;
 		if( y > yMax ) yMax = y;
 		if( y > yMax ) yMax = y;
+		
+		state.add(4);
 	}
 	}
 
 
 	public inline function fillArc( x : Float, y : Float, ray : Float, c : Int, start: Float, end: Float) {
 	public inline function fillArc( x : Float, y : Float, ray : Float, c : Int, start: Float, end: Float) {
@@ -238,6 +259,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		var _x = 0.;
 		var _x = 0.;
 		var _y = 0.;
 		var _y = 0.;
 		var i = 0;
 		var i = 0;
+		var count = 0;
 		while ( i < nsegments ) {
 		while ( i < nsegments ) {
 			var a = start + i * angle;
 			var a = start + i * angle;
 			_x = x + Math.cos(a) * ray;
 			_x = x + Math.cos(a) * ray;
@@ -247,6 +269,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 				addPoint(_x, _y, c);
 				addPoint(_x, _y, c);
 				addPoint(prevX, prevY, c);
 				addPoint(prevX, prevY, c);
 				addPoint(prevX, prevY, c);
 				addPoint(prevX, prevY, c);
+				count += 4;
 			}
 			}
 			prevX = _x;
 			prevX = _x;
 			prevY = _y;
 			prevY = _y;
@@ -259,6 +282,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		addPoint(_x, _y, c);
 		addPoint(_x, _y, c);
 		addPoint(prevX, prevY, c);
 		addPoint(prevX, prevY, c);
 		addPoint(prevX, prevY, c);
 		addPoint(prevX, prevY, c);
+		state.add(count + 4);
 	}
 	}
 
 
 	public inline function fillCircle( x : Float, y : Float, radius : Float, c : Int) {
 	public inline function fillCircle( x : Float, y : Float, radius : Float, c : Int) {
@@ -270,6 +294,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		var firstX = Math.NEGATIVE_INFINITY;
 		var firstX = Math.NEGATIVE_INFINITY;
 		var firstY = Math.NEGATIVE_INFINITY;
 		var firstY = Math.NEGATIVE_INFINITY;
 		var curX = 0., curY = 0.;
 		var curX = 0., curY = 0.;
+		var count = 0;
 		for( i in 0...nsegments) {
 		for( i in 0...nsegments) {
 			var a = i * angle;
 			var a = i * angle;
 			curX = x + Math.cos(a) * radius;
 			curX = x + Math.cos(a) * radius;
@@ -279,9 +304,10 @@ class TileLayerContent extends h3d.prim.Primitive {
 				addPoint(curX, curY, c);
 				addPoint(curX, curY, c);
 				addPoint(prevX, prevY, c);
 				addPoint(prevX, prevY, c);
 				addPoint(x, y, c);
 				addPoint(x, y, c);
+				count += 4;
 			}
 			}
 			if (firstX == Math.NEGATIVE_INFINITY) {
 			if (firstX == Math.NEGATIVE_INFINITY) {
-			firstX = curX;
+				firstX = curX;
 				firstY = curY;
 				firstY = curY;
 			}
 			}
 			prevX = curX;
 			prevX = curX;
@@ -291,6 +317,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		addPoint(curX, curY, c);
 		addPoint(curX, curY, c);
 		addPoint(firstX, firstY, c);
 		addPoint(firstX, firstY, c);
 		addPoint(x, y, c);
 		addPoint(x, y, c);
+		state.add(count + 4);
 	}
 	}
 
 
 	public inline function circle( x : Float, y : Float, ray : Float, size: Float, c : Int) {
 	public inline function circle( x : Float, y : Float, ray : Float, size: Float, c : Int) {
@@ -303,6 +330,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		var prevY = Math.NEGATIVE_INFINITY;
 		var prevY = Math.NEGATIVE_INFINITY;
 		var prevX1 = Math.NEGATIVE_INFINITY;
 		var prevX1 = Math.NEGATIVE_INFINITY;
 		var prevY1 = Math.NEGATIVE_INFINITY;
 		var prevY1 = Math.NEGATIVE_INFINITY;
+		var count = 0;
 		for( i in 0...nsegments ) {
 		for( i in 0...nsegments ) {
 			var a = i * angle;
 			var a = i * angle;
 			var _x = x + Math.cos(a) * ray;
 			var _x = x + Math.cos(a) * ray;
@@ -314,12 +342,14 @@ class TileLayerContent extends h3d.prim.Primitive {
 				addPoint(prevX, prevY, c);
 				addPoint(prevX, prevY, c);
 				addPoint(_x1, _y1, c);
 				addPoint(_x1, _y1, c);
 				addPoint(prevX1, prevY1, c);
 				addPoint(prevX1, prevY1, c);
+				count += 4;
 			}
 			}
 			prevX = _x;
 			prevX = _x;
 			prevY = _y;
 			prevY = _y;
 			prevX1 = _x1;
 			prevX1 = _x1;
 			prevY1 = _y1;
 			prevY1 = _y1;
 		}
 		}
+		state.add(count);
 	}
 	}
 
 
 	public inline function arc( x : Float, y : Float, ray : Float, size: Float, start: Float, end: Float, c : Int) {
 	public inline function arc( x : Float, y : Float, ray : Float, size: Float, start: Float, end: Float, c : Int) {
@@ -338,6 +368,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 		var _y = 0.;
 		var _y = 0.;
 		var _x1 = 0.;
 		var _x1 = 0.;
 		var _y1 = 0.;
 		var _y1 = 0.;
+		var count = 0;
 		for( i in 0...nsegments ) {
 		for( i in 0...nsegments ) {
 			var a = start + i * angle;
 			var a = start + i * angle;
 			_x = x + Math.cos(a) * ray;
 			_x = x + Math.cos(a) * ray;
@@ -349,6 +380,7 @@ class TileLayerContent extends h3d.prim.Primitive {
 				addPoint(prevX, prevY, c);
 				addPoint(prevX, prevY, c);
 				addPoint(_x1, _y1, c);
 				addPoint(_x1, _y1, c);
 				addPoint(prevX1, prevY1, c);
 				addPoint(prevX1, prevY1, c);
+				count += 4;
 			}
 			}
 			prevX = _x;
 			prevX = _x;
 			prevY = _y;
 			prevY = _y;
@@ -364,22 +396,23 @@ class TileLayerContent extends h3d.prim.Primitive {
 		addPoint(prevX, prevY, c);
 		addPoint(prevX, prevY, c);
 		addPoint(_x1, _y1, c);
 		addPoint(_x1, _y1, c);
 		addPoint(prevX1, prevY1, c);
 		addPoint(prevX1, prevY1, c);
+		state.add(count + 4);
 	}
 	}
 
 
 	override public function alloc(engine:h3d.Engine) {
 	override public function alloc(engine:h3d.Engine) {
 		if( tmp == null ) clear();
 		if( tmp == null ) clear();
-		if( tmp.length > 0 )
+		if( tmp.length > 0 ) {
 			buffer = h3d.Buffer.ofFloats(tmp, 8, [Quads, RawFormat]);
 			buffer = h3d.Buffer.ofFloats(tmp, 8, [Quads, RawFormat]);
+		}
 	}
 	}
 
 
 	public inline function flush() {
 	public inline function flush() {
 		if( buffer == null || buffer.isDisposed() ) alloc(h3d.Engine.getCurrent());
 		if( buffer == null || buffer.isDisposed() ) alloc(h3d.Engine.getCurrent());
 	}
 	}
 
 
-	public function doRender(engine:h3d.Engine, min, len) {
+	public inline function doRender(ctx : RenderContext, min, len) {
 		flush();
 		flush();
-		if( buffer != null )
-			engine.renderQuadBuffer(buffer, min, len);
+		state.drawQuads(ctx, buffer, min, len);
 	}
 	}
 
 
 }
 }
@@ -393,7 +426,7 @@ class TileGroup extends Drawable {
 	public var rangeMin : Int;
 	public var rangeMin : Int;
 	public var rangeMax : Int;
 	public var rangeMax : Int;
 
 
-	public function new(t : Tile, ?parent : h2d.Object) {
+	public function new(?t : Tile, ?parent : h2d.Object) {
 		super(parent);
 		super(parent);
 		tile = t;
 		tile = t;
 		rangeMin = rangeMax = -1;
 		rangeMin = rangeMax = -1;
@@ -471,10 +504,10 @@ class TileGroup extends Drawable {
 		var max = content.triCount();
 		var max = content.triCount();
 		if( max == 0 )
 		if( max == 0 )
 			return;
 			return;
-		if( !ctx.beginDrawObject(obj, tile.getTexture()) ) return;
+		if( !ctx.beginDrawBatchState(obj) ) return;
 		var min = rangeMin < 0 ? 0 : rangeMin * 2;
 		var min = rangeMin < 0 ? 0 : rangeMin * 2;
 		if( rangeMax > 0 && rangeMax < max * 2 ) max = rangeMax * 2;
 		if( rangeMax > 0 && rangeMax < max * 2 ) max = rangeMax * 2;
-		content.doRender(ctx.engine, min, max - min);
+		content.doRender(ctx, min, max - min);
 	}
 	}
 
 
 }
 }

+ 205 - 0
h2d/impl/BatchDrawState.hx

@@ -0,0 +1,205 @@
+package h2d.impl;
+
+import h3d.Indexes;
+import h3d.Buffer;
+
+/**
+	Automates buffer segmentation when rendering 2D geometry with multiple unique textures.
+
+	Primary use-case is to allow usage of multiple textures without the need to manually manage them.
+	Causes extra draw call each time a texture is swapped.
+	Due to that, for production it is recommended to combine assets in atlases for optimal performance.
+
+	Depending on geometry type, vertex count should be in groups of 4 vertices per quad or 3 indices per triangle.
+**/
+class BatchDrawState {
+
+	/**
+		Current active texture of the BatchDrawState.
+		Represents the most recent texture that was set with `setTile` or `setTexture`.
+		Always null after state initialization or after `clear` call.
+	**/
+	public var currentTexture(get, never) : h3d.mat.Texture;
+	/**
+		A total amount of vertices added to the BatchDrawState.
+	**/
+	public var totalCount(default, null) : Int;
+
+	var head : StateEntry;
+	var tail : StateEntry;
+
+	/**
+		Create a new BatchDrawState instance.
+	**/
+	public function new() {
+		this.head = this.tail = new StateEntry(null);
+		this.totalCount = 0;
+	}
+
+	/**
+		Switches currently active texture to one in the given `tile` if it differs and splits the render state.
+		@param tile A Tile containing a texture that should be used for the next set of vertices. Does nothing if `null`.
+	**/
+	public inline function setTile( tile : h2d.Tile ) {
+		if ( tile != null ) setTexture(tile.getTexture());
+	}
+
+	/**
+		Switches currently active texture to the given `texture` if it differs and splits the render state.
+		@param texture The texture that should be used for the next set of vertices. Does nothing if `null`.
+	**/
+	public function setTexture( texture : h3d.mat.Texture ) {
+		if ( texture != null ) {
+			if ( tail.texture == null ) tail.texture = texture;
+			else if ( tail.texture != texture ) {
+				var cur = tail;
+				if ( cur.next == null ) cur.next = tail = new StateEntry(texture);
+				else tail = cur.next.set(texture);
+			}
+		}
+	}
+
+	/**
+		Add vertices to the state using currently active texture.
+		Should be called when rendering buffers add more data in order to properly render the geometry.
+		@param count The amount of vertices to add.
+	**/
+	public inline function add( count : Int ) {
+		tail.count += count;
+		totalCount += count;
+	}
+
+	/**
+		Resets the BatchDrawState by removing all texture references and zeroing vertex counter.
+	**/
+	public function clear() {
+		var state = head;
+		do {
+			state.texture = null;
+			state = state.next;
+		} while ( state != null );
+		tail = head;
+		tail.count = 0;
+		totalCount = 0;
+	}
+
+	/**
+		Renders given buffer as a set of quads. Buffer data should be in groups of 4 vertices per quad.
+		@param ctx The render context which performs the rendering. Rendering object should call `h2d.RenderContext.beginDrawBatchState` before calling `drawQuads`.
+		@param buffer The quad buffer used to render the state.
+		@param offset An optional starting offset of the buffer to render in triangles (2 per quad).
+		@param length An optional maximum limit of triangles to render.
+
+		When `offset` and `length` are not provided or are default values, slightly faster rendering routine is used.
+	**/
+	public function drawQuads( ctx : RenderContext, buffer : Buffer, offset = 0, length = -1 ) {
+		var state = head;
+		var last = tail.next;
+		var engine = ctx.engine;
+		var stateLen : Int;
+		inline function toQuads( count : Int ) return count >> 1;
+
+		if ( offset == 0 && length == -1 ) {
+			// Skip extra logic when not restraining rendering
+			do {
+				ctx.swapTexture(state.texture);
+				stateLen = toQuads(state.count);
+				engine.renderQuadBuffer(buffer, offset, stateLen);
+				offset += stateLen;
+				state = state.next;
+			} while ( state != last );
+		} else {
+			if ( length == -1 ) length = toQuads(totalCount) - offset;
+			var caret = 0;
+			do {
+				stateLen = toQuads(state.count);
+				if ( caret + stateLen >= offset ) {
+					var stateMin = offset >= caret ? offset : caret;
+					var stateLen = length > stateLen ? stateLen : length;
+					ctx.swapTexture(state.texture);
+					engine.renderQuadBuffer(buffer, stateMin, stateLen);
+					length -= stateLen;
+					if ( length == 0 ) break;
+				}
+				caret += stateLen;
+				state = state.next;
+			} while ( state != last );
+		}
+	}
+
+	/**
+		Renders given indices as a set of triangles. Index data should be in groups of 3 vertices per quad.
+		@param ctx The render context which performs the rendering. Rendering object should call `h2d.RenderContext.beginDrawBatchState` before calling `drawQuads`.
+		@param buffer The vertex buffer used to render the state.
+		@param indices Vertex indices used to render the state.
+		@param offset An optional starting offset of the buffer to render in triangles.
+		@param length An optional maximum limit of triangles to render.
+
+		When `offset` and `length` are not provided or are default values, slightly faster rendering routine is used.
+	**/
+	public function drawIndexed( ctx : RenderContext, buffer : Buffer, indices : Indexes, offset : Int = 0, length : Int = -1 ) {
+		var state = head;
+		var last = tail.next;
+		var engine = ctx.engine;
+		var stateLen : Int;
+		inline function toTris( count : Int ) return Std.int(count / 3);
+
+		if ( offset == 0 && length == -1 ) {
+			// Skip extra logic when not restraining rendering
+			do {
+				ctx.swapTexture(state.texture);
+				stateLen = toTris(state.count);
+				engine.renderIndexed(buffer, indices, offset, stateLen);
+				offset += stateLen;
+				state = state.next;
+			} while ( state != last );
+		} else {
+			if ( length == -1 ) length = toTris(totalCount);
+			var caret = 0;
+			do {
+				stateLen = toTris(state.count);
+				if ( caret + stateLen >= offset ) {
+					var stateMin = offset >= caret ? offset : caret;
+					var stateLen = length > stateLen ? stateLen : length;
+					ctx.swapTexture(state.texture);
+					engine.renderIndexed(buffer, indices, stateMin, stateLen);
+					length -= stateLen;
+					if ( length == 0 ) break;
+				}
+				caret += stateLen;
+				state = state.next;
+			} while ( state != last );
+		}
+	}
+
+
+	inline function get_currentTexture() return tail.texture;
+
+}
+
+private class StateEntry {
+
+	/**
+		Texture associated with draw state instance.
+	**/
+	public var texture : h3d.mat.Texture;
+	/**
+		A size of batch state.
+	**/
+	public var count : Int;
+	
+
+	public var next:StateEntry;
+
+	public function new( texture : h3d.mat.Texture ) {
+		this.texture = texture;
+		this.count = 0;
+	}
+
+	public function set( texture : h3d.mat.Texture ) : StateEntry {
+		this.texture = texture;
+		this.count = 0;
+		return this;
+	}
+
+}

+ 112 - 0
samples/DrawingTiles.hx

@@ -0,0 +1,112 @@
+class DrawingTiles extends SampleApp {
+
+	override function init() {
+		super.init();
+
+		var logo = hxd.Res.hxlogo.toTile();
+		var normalmap = hxd.Res.normalmap.toTile();
+
+		var hbox = new h2d.Flow(fui);
+		hbox.horizontalSpacing = 10;
+
+		// Bitmap renders a singular Tile
+		new h2d.Bitmap(hxd.Res.hxlogo.toTile(), hbox);
+
+		// TileGroup allows to batch-render multiple tiles.
+		// Best performance ahieved when all Tiles are from same Texture.
+		var tilegroup = new h2d.TileGroup(hbox);
+		var tileSize = logo.width / 8;
+		var tiles = logo.gridFlatten(tileSize);
+		hxd.Math.shuffle(tiles);
+		var i = 0;
+		for ( y in 0...8 ) {
+			for ( x in 0...8 ) {
+				tilegroup.add(x * tileSize, y * tileSize, tiles[i++]);
+				if (Math.random() > 0.7) {
+					// TileGroup supports different texture sources.
+					// but each texture swap causes new drawcall,
+					// so it's adviced to use single texture for all group contents.
+					tilegroup.addAlpha(x * tileSize, y * tileSize, 0.2, normalmap.sub((normalmap.width - tileSize) * Math.random(), (normalmap.height - tileSize) * Math.random(), tileSize, tileSize));
+				}
+			}
+		}
+
+		// SpriteBatch also allow to batch-render multiple tiles.
+		// Compared to TileGroup - it's a dynamic tile geometry and reflushed to GPU every frame.
+		// Same drawcall optimizations with unique texture count apply to SpriteBatch.
+		var sprites = new h2d.SpriteBatch(null, hbox);
+		// Causes containing sprites to receieve `update` calls.
+		sprites.hasUpdate = true;
+		// Tells SpriteBatch to calculate scale and rotation for sprites.
+		// More CPU-intensive.
+		sprites.hasRotationScale = true;
+		tiles = logo.gridFlatten(tileSize);
+		var i = 0;
+		for ( y in 0...8 ) {
+			for ( x in 0...8 ) {
+				var s = new CustomSprite(tiles[i++].center());
+				s.x = x * tileSize + tileSize * .5;
+				s.y = y * tileSize + tileSize * .5;
+				sprites.add(s);
+				if ( Math.random() > 0.7 ) {
+					var o = new CustomSprite(normalmap.sub((normalmap.width - tileSize) * Math.random(), (normalmap.height - tileSize) * Math.random(), tileSize, tileSize, -tileSize*.5, -tileSize*.5));
+					o.x = x * tileSize + tileSize * .5;
+					o.y = y * tileSize + tileSize * .5;
+					o.alpha = 0.2;
+					o.offset = s.offset;
+					if (s.effect == 2) o.effect = 0;
+					else o.effect = s.effect;
+					sprites.add(o);
+				}
+			}
+		}
+
+		// h2d.Graphics can render tiles along with other types of graphics.
+		var g = new h2d.Graphics(fui);
+		g.drawTile(0, 0, logo);
+		// Make drawn textures to wrap UV around.
+		// In this tile fill, it starts at 0-0, and drawn outside texture boundaries,
+		// if tileWrap is off, it will cause it to render borders of the logo.
+		g.tileWrap = true;
+		g.beginTileFill(0, 0, 1, 1, logo);
+		var ow = logo.width;
+		for (pt in [[65, 41], [97, 41], [128, 57], [159, 41], [191, 41], [191, 73], [175, 104], [191, 136], [191, 168],
+								[159, 168], [128, 152], [97, 168], [65, 168], [65, 168], [65, 136], [81, 104], [65, 73]]) {
+			g.lineTo(ow + pt[0], pt[1]);
+		}
+		g.drawRect(ow + 64, 183, 129, 34);
+	}
+
+	static function main() {
+		hxd.Res.initEmbed();
+		new DrawingTiles();
+	}
+
+}
+
+class CustomSprite extends h2d.SpriteBatch.BatchElement {
+
+	public var effect : Int;
+	public var offset : Float;
+
+	public function new( t ) {
+		super(t);
+		effect = Std.random(4);
+		offset = Math.random();
+	}
+
+	override function update( et : Float ):Bool {
+		switch ( effect ) {
+			case 0:
+				scale = Math.sin(hxd.Timer.lastTimeStamp + offset);
+			case 1:
+				rotation += et;
+			case 2:
+				alpha = (hxd.Timer.lastTimeStamp + offset) % 1;
+			case 3:
+				t.setCenterRatio(Math.cos(hxd.Timer.lastTimeStamp + offset), Math.sin(hxd.Timer.lastTimeStamp + offset));
+		}
+		return true;
+	}
+
+}