image/rectangle: read pixels only for necessary parts

Closes #2274
This commit is contained in:
Hajime Hoshi 2022-08-27 21:22:31 +09:00
parent de35a5a6f1
commit b2f874a244
12 changed files with 77 additions and 36 deletions

View File

@ -534,11 +534,15 @@ func (c *writePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexO
type readPixelsCommand struct {
result []byte
img *Image
x int
y int
width int
height int
}
// Exec executes a readPixelsCommand.
func (c *readPixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
if err := c.img.image.ReadPixels(c.result); err != nil {
if err := c.img.image.ReadPixels(c.result, c.x, c.y, c.width, c.height); err != nil {
return err
}
return nil

View File

@ -151,10 +151,14 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, offsets [g
// ReadPixels reads the image's pixels.
// ReadPixels returns an error when an error happens in the graphics driver.
func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, buf []byte) error {
func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, buf []byte, x, y, width, height int) error {
i.resolveBufferedWritePixels()
c := &readPixelsCommand{
img: i,
x: x,
y: y,
width: width,
height: height,
result: buf,
}
theCommandQueue.Enqueue(c)
@ -200,7 +204,7 @@ func (i *Image) dumpTo(w io.Writer, graphicsDriver graphicsdriver.Graphics, blac
}
pix := make([]byte, 4*i.width*i.height)
if err := i.ReadPixels(graphicsDriver, pix); err != nil {
if err := i.ReadPixels(graphicsDriver, pix, 0, 0, i.width, i.height); err != nil {
return err
}

View File

@ -57,7 +57,7 @@ func TestClear(t *testing.T) {
dst.DrawTriangles([graphics.ShaderImageCount]*graphicscommand.Image{src}, [graphics.ShaderImageCount - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeClear, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
pix := make([]byte, 4*w*h)
if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), pix); err != nil {
if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), pix, 0, 0, w, h); err != nil {
t.Fatal(err)
}
for j := 0; j < h/2; j++ {
@ -111,7 +111,7 @@ func TestShader(t *testing.T) {
dst.DrawTriangles([graphics.ShaderImageCount]*graphicscommand.Image{}, [graphics.ShaderImageCount - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, s, nil, false)
pix := make([]byte, 4*w*h)
if err := dst.ReadPixels(g, pix); err != nil {
if err := dst.ReadPixels(g, pix, 0, 0, w, h); err != nil {
t.Fatal(err)
}
for j := 0; j < h; j++ {

View File

@ -1528,7 +1528,7 @@ func (i *Image) ensureReadingStagingBuffer() error {
return nil
}
func (i *Image) ReadPixels(buf []byte) error {
func (i *Image) ReadPixels(buf []byte, x, y, width, height int) error {
if i.screen {
return errors.New("directx: Pixels cannot be called on the screen")
}
@ -1562,12 +1562,12 @@ func (i *Image) ReadPixels(buf []byte) error {
}
i.graphics.needFlushCopyCommandList = true
i.graphics.copyCommandList.CopyTextureRegion_PlacedFootPrint_SubresourceIndex(
&dst, 0, 0, 0, &src, &_D3D12_BOX{
&dst, uint32(x), uint32(y), 0, &src, &_D3D12_BOX{
left: 0,
top: 0,
front: 0,
right: uint32(i.width),
bottom: uint32(i.height),
right: uint32(width),
bottom: uint32(height),
back: 1,
})
@ -1581,8 +1581,8 @@ func (i *Image) ReadPixels(buf []byte) error {
h.Len = int(i.totalBytes)
h.Cap = int(i.totalBytes)
for j := 0; j < i.height; j++ {
copy(buf[j*i.width*4:(j+1)*i.width*4], dstBytes[j*int(i.layouts.Footprint.RowPitch):])
for j := 0; j < height; j++ {
copy(buf[j*width*4:(j+1)*width*4], dstBytes[j*int(i.layouts.Footprint.RowPitch):])
}
i.readingStagingBuffer.Unmap(0, nil)

View File

@ -69,7 +69,7 @@ type Image interface {
ID() ImageID
Dispose()
IsInvalidated() bool
ReadPixels(buf []byte) error
ReadPixels(buf []byte, x, y, width, height int) error
WritePixels(args []*WritePixelsArgs) error
}

View File

@ -1104,16 +1104,17 @@ func (i *Image) syncTexture() {
cb.WaitUntilCompleted()
}
func (i *Image) ReadPixels(buf []byte) error {
if got, want := len(buf), 4*i.width*i.height; got != want {
func (i *Image) ReadPixels(buf []byte, x, y, width, height int) error {
if got, want := len(buf), 4*width*height; got != want {
return fmt.Errorf("metal: len(buf) must be %d but %d at ReadPixels", want, got)
}
i.graphics.flushIfNeeded(false)
i.syncTexture()
i.texture.GetBytes(&buf[0], uintptr(4*i.width), mtl.Region{
Size: mtl.Size{Width: i.width, Height: i.height, Depth: 1},
i.texture.GetBytes(&buf[0], uintptr(4*width), mtl.Region{
Origin: mtl.Origin{X: x, Y: y},
Size: mtl.Size{Width: width, Height: height, Depth: 1},
}, 0)
return nil
}

View File

@ -162,10 +162,10 @@ func (c *context) bindFramebufferImpl(f framebufferNative) {
gl.BindFramebufferEXT(gl.FRAMEBUFFER, uint32(f))
}
func (c *context) framebufferPixels(buf []byte, f *framebuffer, width, height int) {
func (c *context) framebufferPixels(buf []byte, f *framebuffer, x, y, width, height int) {
gl.Flush()
c.bindFramebuffer(f.native)
gl.ReadPixels(0, 0, int32(width), int32(height), gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(buf))
gl.ReadPixels(int32(x), int32(y), int32(width), int32(height), gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(buf))
}
func (c *context) framebufferPixelsToBuffer(f *framebuffer, buffer buffer, width, height int) {

View File

@ -233,14 +233,14 @@ func (c *context) bindFramebufferImpl(f framebufferNative) {
gl.bindFramebuffer.Invoke(gles.FRAMEBUFFER, js.Value(f))
}
func (c *context) framebufferPixels(buf []byte, f *framebuffer, width, height int) {
func (c *context) framebufferPixels(buf []byte, f *framebuffer, x, y, width, height int) {
gl := c.gl
c.bindFramebuffer(f.native)
l := 4 * width * height
p := jsutil.TemporaryUint8ArrayFromUint8Slice(l, nil)
gl.readPixels.Invoke(0, 0, width, height, gles.RGBA, gles.UNSIGNED_BYTE, p)
gl.readPixels.Invoke(x, y, width, height, gles.RGBA, gles.UNSIGNED_BYTE, p)
copy(buf, uint8ArrayToSlice(p, l))
}

View File

@ -148,12 +148,12 @@ func (c *context) bindFramebufferImpl(f framebufferNative) {
c.ctx.BindFramebuffer(gles.FRAMEBUFFER, uint32(f))
}
func (c *context) framebufferPixels(buf []byte, f *framebuffer, width, height int) {
func (c *context) framebufferPixels(buf []byte, f *framebuffer, x, y, width, height int) {
c.ctx.Flush()
c.bindFramebuffer(f.native)
c.ctx.ReadPixels(buf, 0, 0, int32(width), int32(height), gles.RGBA, gles.UNSIGNED_BYTE)
c.ctx.ReadPixels(buf, int32(x), int32(y), int32(width), int32(height), gles.RGBA, gles.UNSIGNED_BYTE)
}
func (c *context) framebufferPixelsToBuffer(f *framebuffer, buffer buffer, width, height int) {

View File

@ -62,12 +62,12 @@ func (i *Image) setViewport() error {
return nil
}
func (i *Image) ReadPixels(buf []byte) error {
func (i *Image) ReadPixels(buf []byte, x, y, width, height int) error {
if err := i.ensureFramebuffer(); err != nil {
return err
}
i.graphics.context.framebufferPixels(buf, i.framebuffer, i.width, i.height)
i.graphics.context.framebufferPixels(buf, i.framebuffer, x, y, width, height)
return nil
}

View File

@ -64,6 +64,13 @@ func (p *Pixels) ReadPixels(pixels []byte, x, y, width, height, imageWidth, imag
p.pixelsRecords.readPixels(pixels, x, y, width, height, imageWidth, imageHeight)
}
func (p *Pixels) Region() image.Rectangle {
if p.pixelsRecords == nil {
return image.Rectangle{}
}
return p.pixelsRecords.region()
}
// drawTrianglesHistoryItem is an item for history of draw-image commands.
type drawTrianglesHistoryItem struct {
images [graphics.ShaderImageCount]*Image
@ -115,6 +122,10 @@ type Image struct {
// stale indicates whether the image needs to be synced with GPU as soon as possible.
stale bool
// staleRegion indicates the region to restore.
// staleRegion is valid only when stale is true.
staleRegion image.Rectangle
imageType ImageType
// priority indicates whether the image is restored in high priority when context-lost happens.
@ -255,10 +266,12 @@ func (i *Image) BasePixelsForTesting() *Pixels {
}
// makeStale makes the image stale.
func (i *Image) makeStale() {
func (i *Image) makeStale(rect image.Rectangle) {
i.stale = true
i.staleRegion = i.staleRegion.Union(i.basePixels.Region()).Union(rect)
i.basePixels = Pixels{}
i.clearDrawTrianglesHistory()
i.stale = true
// Don't have to call makeStale recursively here.
// Restoring is done after topological sorting is done.
@ -301,7 +314,7 @@ func (i *Image) WritePixels(pixels []byte, x, y, width, height int) {
}
if !needsRestoring() || !i.needsRestoring() {
i.makeStale()
i.makeStale(image.Rect(x, y, x+width, y+height))
return
}
@ -317,6 +330,7 @@ func (i *Image) WritePixels(pixels []byte, x, y, width, height int) {
}
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegion = image.Rectangle{}
return
}
@ -375,7 +389,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, offsets [g
}
if srcstale || !needsRestoring() || !i.needsRestoring() {
i.makeStale()
i.makeStale(image.Rect(0, 0, i.width, i.height))
} else {
i.appendDrawTrianglesHistory(srcs, offsets, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, shader, uniforms, evenOdd)
}
@ -406,7 +420,7 @@ func (i *Image) appendDrawTrianglesHistory(srcs [graphics.ShaderImageCount]*Imag
// TODO: Would it be possible to merge draw image history items?
const maxDrawTrianglesHistoryCount = 1024
if len(i.drawTrianglesHistory)+1 > maxDrawTrianglesHistoryCount {
i.makeStale()
i.makeStale(image.Rect(0, 0, i.width, i.height))
return
}
// All images must be resolved and not stale each after frame.
@ -465,7 +479,7 @@ func (i *Image) makeStaleIfDependingOn(target *Image) {
return
}
if i.dependsOn(target) {
i.makeStale()
i.makeStale(image.Rect(0, 0, i.width, i.height))
}
}
@ -475,20 +489,27 @@ func (i *Image) makeStaleIfDependingOnShader(shader *Shader) {
return
}
if i.dependsOnShader(shader) {
i.makeStale()
i.makeStale(image.Rect(0, 0, i.width, i.height))
}
}
// readPixelsFromGPU reads the pixels from GPU and resolves the image's 'stale' state.
func (i *Image) readPixelsFromGPU(graphicsDriver graphicsdriver.Graphics) error {
pix := make([]byte, 4*i.width*i.height)
if err := i.image.ReadPixels(graphicsDriver, pix); err != nil {
return err
if !i.stale {
panic("restorable: the image must be stale at readPixelsFromGPU")
}
i.basePixels = Pixels{}
i.basePixels.AddOrReplace(pix, 0, 0, i.width, i.height)
if r := i.staleRegion; !r.Empty() {
pix := make([]byte, 4*r.Dx()*r.Dy())
if err := i.image.ReadPixels(graphicsDriver, pix, r.Min.X, r.Min.Y, r.Dx(), r.Dy()); err != nil {
return err
}
i.basePixels.AddOrReplace(pix, r.Min.X, r.Min.Y, r.Dx(), r.Dy())
}
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegion = image.Rectangle{}
return nil
}
@ -566,6 +587,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
i.basePixels = Pixels{}
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegion = image.Rectangle{}
return nil
case ImageTypeVolatile:
i.image = graphicscommand.NewImage(w, h, false)
@ -608,7 +630,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
if len(i.drawTrianglesHistory) > 0 {
i.basePixels = Pixels{}
pix := make([]byte, 4*w*h)
if err := gimg.ReadPixels(graphicsDriver, pix); err != nil {
if err := gimg.ReadPixels(graphicsDriver, pix, 0, 0, w, h); err != nil {
return err
}
i.basePixels.AddOrReplace(pix, 0, 0, w, h)
@ -617,6 +639,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
i.image = gimg
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegion = image.Rectangle{}
return nil
}
@ -630,6 +653,7 @@ func (i *Image) Dispose() {
i.basePixels = Pixels{}
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegion = image.Rectangle{}
}
// isInvalidated returns a boolean value indicating whether the image is invalidated.

View File

@ -128,3 +128,11 @@ func (pr *pixelsRecords) apply(img *graphicscommand.Image) {
img.WritePixels(r.pix, r.rect.Min.X, r.rect.Min.Y, r.rect.Dx(), r.rect.Dy())
}
}
func (pr *pixelsRecords) region() image.Rectangle {
var rect image.Rectangle
for _, r := range pr.records {
rect = rect.Union(r.rect)
}
return rect
}