internal/graphicsdriver: render various destination regions as one command

Closes #2232
This commit is contained in:
Hajime Hoshi 2022-11-04 16:17:40 +09:00
parent 1ce29e2afa
commit d73e8f785d
8 changed files with 218 additions and 100 deletions

78
examples/subimage/main.go Normal file
View File

@ -0,0 +1,78 @@
// Copyright 2022 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"image"
"image/color"
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
const (
cx = 32
cy = 32
)
type Game struct {
offscreen *ebiten.Image
subImages [cx][cy]*ebiten.Image
}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
if g.offscreen == nil {
g.offscreen = ebiten.NewImage(screen.Size())
}
// Use various sub-images as rendering destination.
// This is a proof-of-concept of efficient rendering with sub-images (#2232).
sw, sh := screen.Size()
cw := sw / cx
ch := sh / cy
for j := 0; j < cy; j++ {
for i := 0; i < cx; i++ {
img := g.subImages[i][j]
if img == nil {
r := image.Rect(cw*i, ch*j, cw*(i+1), ch*(j+1))
img = g.offscreen.SubImage(r).(*ebiten.Image)
g.subImages[i][j] = img
}
clr := color.RGBA{byte(0xff * float64(i) / cx), byte(0xff * float64(j) / cx), 0, 0xff}
img.Fill(clr)
}
}
screen.DrawImage(g.offscreen, nil)
ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f, TPS: %0.2f", ebiten.ActualFPS(), ebiten.ActualTPS()))
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return 640, 480
}
func main() {
ebiten.SetWindowTitle("Sub-images as rendering destinations (Ebitengine Demo)")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}

View File

@ -749,9 +749,8 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
// If a sub-image is used as a rendering source, the image is used as if it is a small image.
// If a sub-image is used as a rendering destination, the region being rendered is clipped.
//
// Successive uses of multiple various regions as rendering destination might not be efficient,
// even though all the underlying images are the same.
// It's because such renderings cannot be unified into one internal draw command.
// Successive uses of multiple various regions as rendering destination is efficient
// when all the underlying images are the same.
func (i *Image) SubImage(r image.Rectangle) image.Image {
i.copyCheck()
if i.isDisposed() {

View File

@ -71,7 +71,11 @@ type commandQueue struct {
drawTrianglesCommandPool drawTrianglesCommandPool
float32sBuffer []float32
// float32sBuffer is a reusable buffer to allocate []float32.
// float32sBuffer's index is switched at the end of the frame,
// and the buffer of the original index is kept until the next frame ends.
float32sBuffer [2][]float32
float32sBufferIndex int
}
// theCommandQueue is the command queue for the current process.
@ -118,9 +122,16 @@ func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.Sh
// TODO: If dst is the screen, reorder the command to be the last.
if !split && 0 < len(q.commands) {
if last, ok := q.commands[len(q.commands)-1].(*drawTrianglesCommand); ok {
if last.CanMergeWithDrawTrianglesCommand(dst, srcs, vertices, blend, dstRegion, shader, uniforms, evenOdd) {
if last.CanMergeWithDrawTrianglesCommand(dst, srcs, vertices, blend, shader, uniforms, evenOdd) {
last.setVertices(q.lastVertices(len(vertices) + last.numVertices()))
last.addNumIndices(len(indices))
if last.dstRegions[len(last.dstRegions)-1].Region == dstRegion {
last.dstRegions[len(last.dstRegions)-1].IndexCount += len(indices)
} else {
last.dstRegions = append(last.dstRegions, graphicsdriver.DstRegion{
Region: dstRegion,
IndexCount: len(indices),
})
}
return
}
}
@ -130,9 +141,13 @@ func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.Sh
c.dst = dst
c.srcs = srcs
c.vertices = q.lastVertices(len(vertices))
c.nindices = len(indices)
c.blend = blend
c.dstRegion = dstRegion
c.dstRegions = []graphicsdriver.DstRegion{
{
Region: dstRegion,
IndexCount: len(indices),
},
}
c.shader = shader
c.uniforms = uniforms
c.evenOdd = evenOdd
@ -157,7 +172,9 @@ func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bo
err = q.flush(graphicsDriver, endFrame)
})
if endFrame {
q.float32sBuffer = q.float32sBuffer[:0]
q.float32sBuffer[q.float32sBufferIndex] = q.float32sBuffer[q.float32sBufferIndex][:0]
q.float32sBufferIndex++
q.float32sBufferIndex %= len(q.float32sBuffer)
}
return
}
@ -252,15 +269,14 @@ func FlushCommands(graphicsDriver graphicsdriver.Graphics, endFrame bool) error
// drawTrianglesCommand represents a drawing command to draw an image on another image.
type drawTrianglesCommand struct {
dst *Image
srcs [graphics.ShaderImageCount]*Image
vertices []float32
nindices int
blend graphicsdriver.Blend
dstRegion graphicsdriver.Region
shader *Shader
uniforms [][]float32
evenOdd bool
dst *Image
srcs [graphics.ShaderImageCount]*Image
vertices []float32
blend graphicsdriver.Blend
dstRegions []graphicsdriver.DstRegion
shader *Shader
uniforms [][]float32
evenOdd bool
}
func (c *drawTrianglesCommand) String() string {
@ -290,15 +306,13 @@ func (c *drawTrianglesCommand) String() string {
}
}
r := fmt.Sprintf("(x:%d, y:%d, width:%d, height:%d)",
int(c.dstRegion.X), int(c.dstRegion.Y), int(c.dstRegion.Width), int(c.dstRegion.Height))
return fmt.Sprintf("draw-triangles: dst: %s <- src: [%s], dst region: %s, num of indices: %d, blend: %s, even-odd: %t", dst, strings.Join(srcstrs[:], ", "), r, c.nindices, blend, c.evenOdd)
return fmt.Sprintf("draw-triangles: dst: %s <- src: [%s], num of dst regions: %d, num of indices: %d, blend: %s, even-odd: %t", dst, strings.Join(srcstrs[:], ", "), len(c.dstRegions), c.numIndices(), blend, c.evenOdd)
}
// Exec executes the drawTrianglesCommand.
func (c *drawTrianglesCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
// TODO: Is it ok not to bind any framebuffer here?
if c.nindices == 0 {
if len(c.dstRegions) == 0 {
return nil
}
@ -311,7 +325,7 @@ func (c *drawTrianglesCommand) Exec(graphicsDriver graphicsdriver.Graphics, inde
imgs[i] = src.image.ID()
}
return graphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.shader.shader.ID(), c.nindices, indexOffset, c.blend, c.dstRegion, c.uniforms, c.evenOdd)
return graphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.shader.shader.ID(), c.dstRegions, indexOffset, c.blend, c.uniforms, c.evenOdd)
}
func (c *drawTrianglesCommand) numVertices() int {
@ -319,20 +333,20 @@ func (c *drawTrianglesCommand) numVertices() int {
}
func (c *drawTrianglesCommand) numIndices() int {
return c.nindices
var nindices int
for _, dstRegion := range c.dstRegions {
nindices += dstRegion.IndexCount
}
return nindices
}
func (c *drawTrianglesCommand) setVertices(vertices []float32) {
c.vertices = vertices
}
func (c *drawTrianglesCommand) addNumIndices(n int) {
c.nindices += n
}
// CanMergeWithDrawTrianglesCommand returns a boolean value indicating whether the other drawTrianglesCommand can be merged
// with the drawTrianglesCommand c.
func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageCount]*Image, vertices []float32, blend graphicsdriver.Blend, dstRegion graphicsdriver.Region, shader *Shader, uniforms [][]float32, evenOdd bool) bool {
func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageCount]*Image, vertices []float32, blend graphicsdriver.Blend, shader *Shader, uniforms [][]float32, evenOdd bool) bool {
if c.shader != shader {
return false
}
@ -358,9 +372,6 @@ func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs
if c.blend != blend {
return false
}
if c.dstRegion != dstRegion {
return false
}
if c.evenOdd || evenOdd {
if c.evenOdd && evenOdd {
return !mightOverlapDstRegions(c.vertices, vertices)
@ -580,12 +591,12 @@ func roundUpPower2(x int) int {
}
func (q *commandQueue) allocFloat32s(n int) []float32 {
buf := q.float32sBuffer
buf := q.float32sBuffer[q.float32sBufferIndex]
if len(buf)+n > cap(buf) {
buf = make([]float32, 0, max(roundUpPower2(len(buf)+n), 16))
}
s := buf[len(buf) : len(buf)+n]
q.float32sBuffer = buf[:len(buf)+n]
q.float32sBuffer[q.float32sBufferIndex] = buf[:len(buf)+n]
return s
}

View File

@ -1168,7 +1168,7 @@ func (g *Graphics) NewShader(program *shaderir.Program) (graphicsdriver.Shader,
return s, nil
}
func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.ShaderImageCount]graphicsdriver.ImageID, shaderID graphicsdriver.ShaderID, indexLen int, indexOffset int, blend graphicsdriver.Blend, dstRegion graphicsdriver.Region, uniforms [][]float32, evenOdd bool) error {
func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.ShaderImageCount]graphicsdriver.ImageID, shaderID graphicsdriver.ShaderID, dstRegions []graphicsdriver.DstRegion, indexOffset int, blend graphicsdriver.Blend, uniforms [][]float32, evenOdd bool) error {
if shaderID == graphicsdriver.InvalidShaderID {
return fmt.Errorf("directx: shader ID is invalid")
}
@ -1239,15 +1239,6 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh
MaxDepth: _D3D12_MAX_DEPTH,
},
})
g.drawCommandList.RSSetScissorRects([]_D3D12_RECT{
{
left: int32(dstRegion.X),
top: int32(dstRegion.Y),
right: int32(dstRegion.X + dstRegion.Width),
bottom: int32(dstRegion.Y + dstRegion.Height),
},
})
g.drawCommandList.IASetPrimitiveTopology(_D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST)
g.drawCommandList.IASetVertexBuffers(0, []_D3D12_VERTEX_BUFFER_VIEW{
{
@ -1262,7 +1253,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcs [graphics.Sh
Format: _DXGI_FORMAT_R16_UINT,
})
if err := g.pipelineStates.drawTriangles(g.device, g.drawCommandList, g.frameIndex, dst.screen, srcImages, shader, flattenUniforms, blend, indexLen, indexOffset, evenOdd); err != nil {
if err := g.pipelineStates.drawTriangles(g.device, g.drawCommandList, g.frameIndex, dst.screen, srcImages, shader, dstRegions, flattenUniforms, blend, indexOffset, evenOdd); err != nil {
return err
}

View File

@ -146,7 +146,7 @@ func (p *pipelineStates) initialize(device *_ID3D12Device) (ferr error) {
return nil
}
func (p *pipelineStates) drawTriangles(device *_ID3D12Device, commandList *_ID3D12GraphicsCommandList, frameIndex int, screen bool, srcs [graphics.ShaderImageCount]*Image, shader *Shader, uniforms []float32, blend graphicsdriver.Blend, indexLen int, indexOffset int, evenOdd bool) error {
func (p *pipelineStates) drawTriangles(device *_ID3D12Device, commandList *_ID3D12GraphicsCommandList, frameIndex int, screen bool, srcs [graphics.ShaderImageCount]*Image, shader *Shader, dstRegions []graphicsdriver.DstRegion, uniforms []float32, blend graphicsdriver.Blend, indexOffset int, evenOdd bool) error {
idx := len(p.constantBuffers[frameIndex])
if idx >= numDescriptorsPerFrame {
return fmt.Errorf("directx: too many constant buffers")
@ -255,27 +255,38 @@ func (p *pipelineStates) drawTriangles(device *_ID3D12Device, commandList *_ID3D
}
commandList.SetGraphicsRootDescriptorTable(2, sh)
if evenOdd {
s, err := shader.pipelineState(blend, prepareStencil, screen)
if err != nil {
return err
}
commandList.SetPipelineState(s)
commandList.DrawIndexedInstanced(uint32(indexLen), 1, uint32(indexOffset), 0, 0)
for _, dstRegion := range dstRegions {
commandList.RSSetScissorRects([]_D3D12_RECT{
{
left: int32(dstRegion.Region.X),
top: int32(dstRegion.Region.Y),
right: int32(dstRegion.Region.X + dstRegion.Region.Width),
bottom: int32(dstRegion.Region.Y + dstRegion.Region.Height),
},
})
if evenOdd {
s, err := shader.pipelineState(blend, prepareStencil, screen)
if err != nil {
return err
}
commandList.SetPipelineState(s)
commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0)
s, err = shader.pipelineState(blend, drawWithStencil, screen)
if err != nil {
return err
s, err = shader.pipelineState(blend, drawWithStencil, screen)
if err != nil {
return err
}
commandList.SetPipelineState(s)
commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0)
} else {
s, err := shader.pipelineState(blend, noStencil, screen)
if err != nil {
return err
}
commandList.SetPipelineState(s)
commandList.DrawIndexedInstanced(uint32(dstRegion.IndexCount), 1, uint32(indexOffset), 0, 0)
}
commandList.SetPipelineState(s)
commandList.DrawIndexedInstanced(uint32(indexLen), 1, uint32(indexOffset), 0, 0)
} else {
s, err := shader.pipelineState(blend, noStencil, screen)
if err != nil {
return err
}
commandList.SetPipelineState(s)
commandList.DrawIndexedInstanced(uint32(indexLen), 1, uint32(indexOffset), 0, 0)
indexOffset += dstRegion.IndexCount
}
return nil

View File

@ -28,6 +28,11 @@ type Region struct {
Height float32
}
type DstRegion struct {
Region Region
IndexCount int
}
const (
InvalidImageID = 0
InvalidShaderID = 0
@ -52,7 +57,7 @@ type Graphics interface {
NewShader(program *shaderir.Program) (Shader, error)
// DrawTriangles draws an image onto another image with the given parameters.
DrawTriangles(dst ImageID, srcs [graphics.ShaderImageCount]ImageID, shader ShaderID, indexLen int, indexOffset int, blend Blend, dstRegion Region, uniforms [][]float32, evenOdd bool) error
DrawTriangles(dst ImageID, srcs [graphics.ShaderImageCount]ImageID, shader ShaderID, dstRegions []DstRegion, indexOffset int, blend Blend, uniforms [][]float32, evenOdd bool) error
}
// GraphicsNotReady represents that the graphics driver is not ready for recovering from the context lost.

View File

@ -421,7 +421,7 @@ func (g *Graphics) flushRenderCommandEncoderIfNeeded() {
g.lastDst = nil
}
func (g *Graphics) draw(dst *Image, dstRegion graphicsdriver.Region, srcs [graphics.ShaderImageCount]*Image, indexLen int, indexOffset int, shader *Shader, uniforms [][]float32, blend graphicsdriver.Blend, evenOdd bool) error {
func (g *Graphics) draw(dst *Image, dstRegions []graphicsdriver.DstRegion, srcs [graphics.ShaderImageCount]*Image, indexOffset int, shader *Shader, uniforms [][]float32, blend graphicsdriver.Blend, evenOdd bool) error {
// When prepareing a stencil buffer, flush the current render command encoder
// to make sure the stencil buffer is cleared when loading.
// TODO: What about clearing the stencil buffer by vertices?
@ -473,12 +473,6 @@ func (g *Graphics) draw(dst *Image, dstRegion graphicsdriver.Region, srcs [graph
ZNear: -1,
ZFar: 1,
})
g.rce.SetScissorRect(mtl.ScissorRect{
X: int(dstRegion.X),
Y: int(dstRegion.Y),
Width: int(dstRegion.Width),
Height: int(dstRegion.Height),
})
g.rce.SetVertexBuffer(g.vb, 0, 0)
for i, u := range uniforms {
@ -497,38 +491,60 @@ func (g *Graphics) draw(dst *Image, dstRegion graphicsdriver.Region, srcs [graph
}
}
var (
prepareStencilRpss mtl.RenderPipelineState
drawWithStencilRpss mtl.RenderPipelineState
noStencilRpss mtl.RenderPipelineState
)
if evenOdd {
prepareStencilRpss, err := shader.RenderPipelineState(&g.view, blend, prepareStencil, dst.screen)
s, err := shader.RenderPipelineState(&g.view, blend, prepareStencil, dst.screen)
if err != nil {
return err
}
drawWithStencilRpss, err := shader.RenderPipelineState(&g.view, blend, drawWithStencil, dst.screen)
prepareStencilRpss = s
s, err = shader.RenderPipelineState(&g.view, blend, drawWithStencil, dst.screen)
if err != nil {
return err
}
g.rce.SetDepthStencilState(g.dsss[prepareStencil])
g.rce.SetRenderPipelineState(prepareStencilRpss)
g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, indexLen, mtl.IndexTypeUInt16, g.ib, indexOffset*2)
g.rce.SetDepthStencilState(g.dsss[drawWithStencil])
g.rce.SetRenderPipelineState(drawWithStencilRpss)
g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, indexLen, mtl.IndexTypeUInt16, g.ib, indexOffset*2)
drawWithStencilRpss = s
} else {
rpss, err := shader.RenderPipelineState(&g.view, blend, noStencil, dst.screen)
s, err := shader.RenderPipelineState(&g.view, blend, noStencil, dst.screen)
if err != nil {
return err
}
noStencilRpss = s
}
g.rce.SetDepthStencilState(g.dsss[noStencil])
g.rce.SetRenderPipelineState(rpss)
g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, indexLen, mtl.IndexTypeUInt16, g.ib, indexOffset*2)
for _, dstRegion := range dstRegions {
g.rce.SetScissorRect(mtl.ScissorRect{
X: int(dstRegion.Region.X),
Y: int(dstRegion.Region.Y),
Width: int(dstRegion.Region.Width),
Height: int(dstRegion.Region.Height),
})
if evenOdd {
g.rce.SetDepthStencilState(g.dsss[prepareStencil])
g.rce.SetRenderPipelineState(prepareStencilRpss)
g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt16, g.ib, indexOffset*2)
g.rce.SetDepthStencilState(g.dsss[drawWithStencil])
g.rce.SetRenderPipelineState(drawWithStencilRpss)
g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt16, g.ib, indexOffset*2)
} else {
g.rce.SetDepthStencilState(g.dsss[noStencil])
g.rce.SetRenderPipelineState(noStencilRpss)
g.rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, dstRegion.IndexCount, mtl.IndexTypeUInt16, g.ib, indexOffset*2)
}
indexOffset += dstRegion.IndexCount
}
return nil
}
func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.ShaderImageCount]graphicsdriver.ImageID, shaderID graphicsdriver.ShaderID, indexLen int, indexOffset int, blend graphicsdriver.Blend, dstRegion graphicsdriver.Region, uniforms [][]float32, evenOdd bool) error {
func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.ShaderImageCount]graphicsdriver.ImageID, shaderID graphicsdriver.ShaderID, dstRegions []graphicsdriver.DstRegion, indexOffset int, blend graphicsdriver.Blend, uniforms [][]float32, evenOdd bool) error {
if shaderID == graphicsdriver.InvalidShaderID {
return fmt.Errorf("metal: shader ID is invalid")
}
@ -586,7 +602,7 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.
}
}
if err := g.draw(dst, dstRegion, srcs, indexLen, indexOffset, g.shaders[shaderID], uniformVars, blend, evenOdd); err != nil {
if err := g.draw(dst, dstRegions, srcs, indexOffset, g.shaders[shaderID], uniformVars, blend, evenOdd); err != nil {
return err
}

View File

@ -180,7 +180,7 @@ func (g *Graphics) uniformVariableName(idx int) string {
return name
}
func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.ShaderImageCount]graphicsdriver.ImageID, shaderID graphicsdriver.ShaderID, indexLen int, indexOffset int, blend graphicsdriver.Blend, dstRegion graphicsdriver.Region, uniforms [][]float32, evenOdd bool) error {
func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.ShaderImageCount]graphicsdriver.ImageID, shaderID graphicsdriver.ShaderID, dstRegions []graphicsdriver.DstRegion, indexOffset int, blend graphicsdriver.Blend, uniforms [][]float32, evenOdd bool) error {
if shaderID == graphicsdriver.InvalidShaderID {
return fmt.Errorf("opengl: shader ID is invalid")
}
@ -192,12 +192,6 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.
if err := destination.setViewport(); err != nil {
return err
}
g.context.scissor(
int(dstRegion.X),
int(dstRegion.Y),
int(dstRegion.Width),
int(dstRegion.Height),
)
g.context.blend(blend)
shader := g.shaders[shaderID]
@ -248,11 +242,24 @@ func (g *Graphics) DrawTriangles(dstID graphicsdriver.ImageID, srcIDs [graphics.
return err
}
g.context.enableStencilTest()
g.context.beginStencilWithEvenOddRule()
g.context.drawElements(indexLen, indexOffset*2)
g.context.endStencilWithEvenOddRule()
}
g.context.drawElements(indexLen, indexOffset*2) // 2 is uint16 size in bytes
for _, dstRegion := range dstRegions {
g.context.scissor(
int(dstRegion.Region.X),
int(dstRegion.Region.Y),
int(dstRegion.Region.Width),
int(dstRegion.Region.Height),
)
if evenOdd {
g.context.beginStencilWithEvenOddRule()
g.context.drawElements(dstRegion.IndexCount, indexOffset*2)
g.context.endStencilWithEvenOddRule()
}
g.context.drawElements(dstRegion.IndexCount, indexOffset*2) // 2 is uint16 size in bytes
indexOffset += dstRegion.IndexCount
}
if evenOdd {
g.context.disableStencilTest()
}