Repository: pion/mediadevices
Pull request #67 (2020-02-17 10:20:48)

Add CGO version of color convert
```
BenchmarkToI420/480p/I444-4         	   17209	    238,403 ns/op
BenchmarkToI420/480p/I422-4         	   24540	    148,271 ns/op
BenchmarkToI420/1080p/I444-4        	    2365	  1,397,549 ns/op
BenchmarkToI420/1080p/I422-4        	    4168	    816,333 ns/op
BenchmarkToI420CGO/NoCGO/480p/I444-4             5720	    570,723 ns/op
BenchmarkToI420CGO/NoCGO/480p/I422-4             8224	    432,129 ns/op
BenchmarkToI420CGO/NoCGO/1080p/I444-4             997	  3,480,059 ns/op
BenchmarkToI420CGO/NoCGO/1080p/I422-4            1335	  2,602,584 ns/op
```
with `CGO_CFLAGS=-march=native -O3`
```
BenchmarkToI420/480p/I444-4         	   47775	     73,395 ns/op
BenchmarkToI420/480p/I422-4         	  109963	     32,822 ns/op
BenchmarkToI420/1080p/I444-4        	    8026	    484,568 ns/op
BenchmarkToI420/1080p/I422-4        	   22366	    157,334 ns/op
```


at-wat: ```
Pure Go
BenchmarkNoCGO/ToI420/1080p/I422-4               405     2,859,911 ns/op
BenchmarkNoCGO/ToI420/1080p/I444-4               344     3,326,899 ns/op
BenchmarkNoCGO/ToI420/1080p/RGBA-4                32    42,419,577 ns/op
BenchmarkNoCGO/ToRGBA/1080p/I420-4                 6   184,056,076 ns/op
BenchmarkNoCGO/ToRGBA/1080p/I422-4                 6   183,812,251 ns/op
BenchmarkNoCGO/ToRGBA/1080p/I444-4                 7   174,262,239 ns/op

CGO (CGO_CFLAGS=-march=native -O2)
BenchmarkToI420/1080p/I422-4          1006       996,511 ns/op
BenchmarkToI420/1080p/I444-4           714     1,849,177 ns/op
BenchmarkToI420/1080p/RGBA-4            82    14,362,118 ns/op
BenchmarkToRGBA/1080p/I420-4             6   181,565,163 ns/op (no cgo impl.)
BenchmarkToRGBA/1080p/I422-4             7   184,278,143 ns/op (no cgo impl.)
BenchmarkToRGBA/1080p/I444-4            84    17,588,926 ns/op

CGO (CGO_CFLAGS=-march=native -O3)
BenchmarkToI420/1080p/I422-4        7815       156,286 ns/op
BenchmarkToI420/1080p/I444-4        2427       452,322 ns/op
BenchmarkToI420/1080p/RGBA-4          92    13,486,092 ns/op
BenchmarkToRGBA/1080p/I420-4           7   142,879,849 ns/op (no cgo impl.)
BenchmarkToRGBA/1080p/I422-4           6   187,748,939 ns/op (no cgo impl.)
BenchmarkToRGBA/1080p/I444-4          60    17,563,576 ns/op
```

Patch:
diff --git a/docs/drawio/driver-lifecycle.drawio b/docs/drawio/driver-lifecycle.drawio
index 84c0a80..a737447 100644
--- a/docs/drawio/driver-lifecycle.drawio
+++ b/docs/drawio/driver-lifecycle.drawio
@@ -1,1 +1,1 @@
-<mxfile host="www.draw.io" modified="2019-12-24T02:41:10.556Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" etag="tOoaduTjj7sv3sJ3j9o2" version="12.4.3" type="device" pages="1"><diagram id="KxQVgcTI2EhjUlQIPq_l" name="Page-1">7VrJkpswEP0aHz0Fktfj2DNJqpJUUvEhyVE2MpAAIkIe2/n6tEAsYvESjzGuxBej1oJEv+7X3dDDc3/3lpPQ+cgs6vWQYe16+KmHkGmaQ/iTkn0iGWGcCGzuWmpQLli4v6kSGkq6cS0aaQMFY55wQ124YkFAV0KTEc7ZVh+2Zp5+15DYtCJYrIhXlX51LeEk0snQyOXvqGs76Z1NQ/X4JB2sBJFDLLYtiPBzD885YyK58ndz6smHlz6XZN6bht5sY5wG4pQJn9+4nv30e/HD+IDe+svtly/++745UJsT+/TE1IIHoJqMC4fZLCDecy6dcbYJLCqXNaCVj/nAWAhCE4Q/qBB7pU2yEQxEjvA91Ut3rvhWuP4ul3oYqtbTTq0cN/aqkexTbq7x/EoUsQ1f0UOHVjgi3KbiwDiUaQngTZlPBd/DPE49ItwXfR9E4czOxuWqgAuljTM0g9AtNZNr43uhp14z8Pz5vjBJNrNZspFPi1s31Oj0Qo3GUx85J/vCgJC5gYgKK3+WAhignOBIOQDlAfG4ZKel4ePJwfFwkWwgh1Z2kgvQptz0C/E26jH00MiDZzlbs3h3OQ5HvzYs7ehHMZIeYYA5CHd5J1zZ8n/usYimS8HWktWSvgrABd0JHY/Ec+0ArlcACcpB8EK5cME9P6oO37WsBPoUtkKW8VISXUorsO5w1hs+ydvhmUeW1JuR1U87tpQ58xiPb43X8U+dKjUypQh5T7rTwKHoRt0vd/JF8B4w6yoE1fLGg4HGQ03/KbjPA2kFVv2RPoOt1xHYStlNvQKWzMuhNKqDUrrKMhV8CmkAzi4H17I89Bjg4DQQSEj4bB1X0EVIYjezhVhGx2EjECreqqpapUeUxgzbPI4wU9w4hRgitf9XpxRz3BmyP5FQinSSE8jV6QSdSCf4pgHCqGJnCwE66axTTQACEefVnWwC9ANO1hziie5k0es4WVPn7v6gLaeL8P2Ei9ajTMygGbCAJhI4knclO7403KsoGRv1AVq2ROI31KwrqLotfl3AQcQdEKyagAa6XjpAuJObGuX5GVyWsrWcv+ETCfemGbk57Uz4dKRW0hBA5el5dzR6aUZ+WQh10+pXR0nz0qC2njSHg9uSJm6PNFkY3g9pDiZdI82sXt4Bozw1T9ULny1mqtMT3WwDCC406lJlMwvAjtj0uQXUckU0fTnUVEEtb8tso4BarXrJ8lQFyv9gNl5421XngfrGg4FLNc8rZeNIX+F62fi0LbaJy+t3QzZj1DmyGVc0FWe9/5jdHokRkF4sw+iAOhpMcaxbYnt1saop/ldwWcFmmWCn96NgbFQU/P/N5XXdAdbJGk/OZuaM4VO4TNvDS93ryUZOLCg0ckgo+yPpPsCHCNm/hhS8oAsj/snBgrOfVNOS6inb+CvQ6rRUkM7eIBdoFdewKr4Wq+K6EvXZOV3h2ZdtZ8mEYL5eF2Ey3I4l6vFOztJDfQ30r2tmx8sszVlfO5+xVIxyUv5QBUNkbuDsVzLRvy7YQDP/6i0Znn87iJ//AA==</diagram></mxfile>
\ No newline at end of file
+<mxfile host="www.draw.io" modified="2020-02-18T04:26:37.746Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0" etag="UfoMaDdl0A5svOeIB0k8" version="12.7.1" type="device"><diagram id="KxQVgcTI2EhjUlQIPq_l" name="Page-1">7VjbbuIwEP0aHqly4frIray0rbYCabs8msQkbk2cdRwIfP2OE4fcCKIK7bar5QHZZ8bjcWbmeJKWOdlGc45895HZmLYMzY5a5rRlGMNOD/4lcEiArtlNAIcTO4H0DFiSI1agptCQ2DgoKArGqCB+EbSY52FLFDDEOdsX1TaMFnf1kYMrwNJCtIo+E1u4CTroahn+DRPHTXfWNSXZolRZAYGLbLbPQeasZU44YyIZbaMJpvLZpc8lWXdfIz05xrEnrlnQmT8NdTpbjOY+e8Srh+fjyGkPlW/ikB4Y23B+NWVcuMxhHqKzDB1zFno2llY1mGU6D4z5AOoAvmAhDiqYKBQMIFdsqZLiiIhfufFKmrrrqtk0UpbjySGdeIIffuUnuVVymi2LZ+m65HzyULWPTUEBC7mltJ7uCXWmx+WL9mDMt+v9YrH93k7TD3EHiwt6xim4UBSYbTH4A+s4pkiQXdEPpNLTOellEYSBCuIbAqqbieEdoqHaqmX0KPg7tskOho4c/oSiYgtsMW7L7ZQCDHM6Z5aNQpucliXSNS/rl62U8kvgSBTTAVHieDC2ICKYA7DDXBAowZESbIltJ5mHA3JE69iUDK7PiCfiJ9gdt7pTQChaYzpG1qsTZ+mEUcbjfc1N/Dt5JPfA0TkKUfazws3nSn0RVQOurLe1O+CLfmLrUNjx6pRQxp/keTPLun7OaLqebTYB5Gk5oU4ONsgxvRLVD2SNjClWeaKoYQ17JC8BmV0UBQGxEhDKNjV7a4JoUPjno9ztaIUwGz2taCIhJLWqUbQv0d4ZQtmw2M8sDXq/Q5YK2kEcyBEo6D0/yoRlvlinwA8fe9jO0cj6DLUke9ZwC5wG+gKZbXuXCLz0URyvPXQmxVyqjXGFFmrLujMohsU0VGOzz9oEPaUQN9ciDLT6DGhG/Vr1gXzWskwv8+z+XuVl73+ZG1de5jXc3rCmO3qppvXrahrIDB1yauoCrN2n3z/PHfc1+mbZr6I+DBIPbkowxkcRzCL0POI5X4Vh+vpnY5jB3ySYL/a2MLySYJo2Dc2ujGrtyVu4EuZ/sGkfXKw9aNr1vt5pNWrTb9qJX0qy96fOCWXB1+nNjO5nY87+f+a8mjnNaz+0NO3Nro3oRS8LtVeb87moBC7ypTyA84mlQELKN/BCmuM9Lf5JZcHZKy4wopLIYlORB566Sdno5bJJ57myMc9Ujfn2qoFp9uUzYcPs87E5+wM=</diagram></mxfile>
\ No newline at end of file
diff --git a/pkg/codec/vpx/vpx.go b/pkg/codec/vpx/vpx.go
index 6dc7935..b9ea017 100644
--- a/pkg/codec/vpx/vpx.go
+++ b/pkg/codec/vpx/vpx.go
@@ -35,8 +35,8 @@ package vpx
 // // Wrap encode function to keep Go memory safe
 // vpx_codec_err_t encode_wrapper(
 //     vpx_codec_ctx_t* codec, vpx_image_t* raw,
-//     long t, ulong dt, long flags, ulong deadline,
-//     u_char *y_ptr, u_char *cb_ptr, u_char *cr_ptr) {
+//     long t, unsigned long dt, long flags, unsigned long deadline,
+//     unsigned char *y_ptr, unsigned char *cb_ptr, unsigned char *cr_ptr) {
 //   raw->planes[0] = y_ptr;
 //   raw->planes[1] = cb_ptr;
 //   raw->planes[2] = cr_ptr;
diff --git a/pkg/io/video/convert.go b/pkg/io/video/convert.go
index 427629e..cd9c0bf 100644
--- a/pkg/io/video/convert.go
+++ b/pkg/io/video/convert.go
@@ -6,6 +6,10 @@ import (
 	"image/color"
 )
 
+var (
+	hasCGOConvert = false
+)
+
 // imageToYCbCr converts src to *image.YCbCr and store it to dst
 // Note: conversion can be lossy
 func imageToYCbCr(dst *image.YCbCr, src image.Image) {
@@ -42,6 +46,10 @@ func imageToYCbCr(dst *image.YCbCr, src image.Image) {
 
 	switch s := src.(type) {
 	case *image.RGBA:
+		if hasCGOConvert {
+			rgbaToI444(dst, s)
+			return
+		}
 		i := 0
 		addr := 0
 		for yi := 0; yi < dy; yi++ {
@@ -70,8 +78,61 @@ func imageToYCbCr(dst *image.YCbCr, src image.Image) {
 	}
 }
 
+func i444ToI420(img *image.YCbCr) {
+	h := img.Rect.Dy()
+	addrSrc0 := 0
+	addrSrc1 := img.CStride
+	addrDst := 0
+	for i := 0; i < h/2; i++ {
+		for j := 0; j < img.CStride/2; j++ {
+			cb := uint16(img.Cb[addrSrc0]) + uint16(img.Cb[addrSrc1]) +
+				uint16(img.Cb[addrSrc0+1]) + uint16(img.Cb[addrSrc1+1])
+			cr := uint16(img.Cr[addrSrc0]) + uint16(img.Cr[addrSrc1]) +
+				uint16(img.Cr[addrSrc0+1]) + uint16(img.Cr[addrSrc1+1])
+			img.Cb[addrDst] = uint8(cb / 4)
+			img.Cr[addrDst] = uint8(cr / 4)
+			addrSrc0 += 2
+			addrSrc1 += 2
+			addrDst++
+		}
+		addrSrc0 += img.CStride
+		addrSrc1 += img.CStride
+	}
+	img.CStride = img.CStride / 2
+	cLen := img.CStride * (h / 2)
+	img.Cb = img.Cb[:cLen]
+	img.Cr = img.Cr[:cLen]
+}
+
+func i422ToI420(img *image.YCbCr) {
+	h := img.Rect.Dy()
+	addrSrc := 0
+	addrDst := 0
+	for i := 0; i < h/2; i++ {
+		for j := 0; j < img.CStride; j++ {
+			cb := uint16(img.Cb[addrSrc]) + uint16(img.Cb[addrSrc+img.CStride])
+			cr := uint16(img.Cr[addrSrc]) + uint16(img.Cr[addrSrc+img.CStride])
+			img.Cb[addrDst] = uint8(cb / 2)
+			img.Cr[addrDst] = uint8(cr / 2)
+			addrDst++
+			addrSrc++
+		}
+		addrSrc += img.CStride
+	}
+	cLen := img.CStride * (h / 2)
+	img.Cb = img.Cb[:cLen]
+	img.Cr = img.Cr[:cLen]
+}
+
 // ToI420 converts r to a new reader that will output images in I420 format
 func ToI420(r Reader) Reader {
+	f444to420 := i444ToI420
+	f422to420 := i422ToI420
+	if hasCGOConvert {
+		f444to420 = i444ToI420CGO
+		f422to420 = i422ToI420CGO
+	}
+
 	var yuvImg image.YCbCr
 	return ReaderFunc(func() (image.Image, error) {
 		img, err := r.Read()
@@ -80,50 +141,13 @@ func ToI420(r Reader) Reader {
 		}
 
 		imageToYCbCr(&yuvImg, img)
-		h := yuvImg.Rect.Dy()
 
 		// Covert pixel format to I420
 		switch yuvImg.SubsampleRatio {
 		case image.YCbCrSubsampleRatio444:
-			addrSrc0 := 0
-			addrSrc1 := yuvImg.CStride
-			addrDst := 0
-			for i := 0; i < h/2; i++ {
-				for j := 0; j < yuvImg.CStride/2; j++ {
-					cb := uint16(yuvImg.Cb[addrSrc0]) + uint16(yuvImg.Cb[addrSrc1]) +
-						uint16(yuvImg.Cb[addrSrc0+1]) + uint16(yuvImg.Cb[addrSrc1+1])
-					cr := uint16(yuvImg.Cr[addrSrc0]) + uint16(yuvImg.Cr[addrSrc1]) +
-						uint16(yuvImg.Cr[addrSrc0+1]) + uint16(yuvImg.Cr[addrSrc1+1])
-					yuvImg.Cb[addrDst] = uint8(cb / 4)
-					yuvImg.Cr[addrDst] = uint8(cr / 4)
-					addrSrc0 += 2
-					addrSrc1 += 2
-					addrDst++
-				}
-				addrSrc0 += yuvImg.CStride
-				addrSrc1 += yuvImg.CStride
-			}
-			yuvImg.CStride = yuvImg.CStride / 2
-			cLen := yuvImg.CStride * (h / 2)
-			yuvImg.Cb = yuvImg.Cb[:cLen]
-			yuvImg.Cr = yuvImg.Cr[:cLen]
+			f444to420(&yuvImg)
 		case image.YCbCrSubsampleRatio422:
-			addrSrc := 0
-			addrDst := 0
-			for i := 0; i < h/2; i++ {
-				for j := 0; j < yuvImg.CStride; j++ {
-					cb := uint16(yuvImg.Cb[addrSrc]) + uint16(yuvImg.Cb[addrSrc+yuvImg.CStride])
-					cr := uint16(yuvImg.Cr[addrSrc]) + uint16(yuvImg.Cr[addrSrc+yuvImg.CStride])
-					yuvImg.Cb[addrDst] = uint8(cb / 2)
-					yuvImg.Cr[addrDst] = uint8(cr / 2)
-					addrDst++
-					addrSrc++
-				}
-				addrSrc += yuvImg.CStride
-			}
-			cLen := yuvImg.CStride * (h / 2)
-			yuvImg.Cb = yuvImg.Cb[:cLen]
-			yuvImg.Cr = yuvImg.Cr[:cLen]
+			f422to420(&yuvImg)
 		case image.YCbCrSubsampleRatio420:
 		default:
 			return nil, fmt.Errorf("unsupported pixel format: %s", yuvImg.SubsampleRatio)
@@ -140,8 +164,7 @@ func imageToRGBA(dst *image.RGBA, src image.Image) {
 		panic("dst can't be nil")
 	}
 
-	srcRGBA, ok := src.(*image.RGBA)
-	if ok {
+	if srcRGBA, ok := src.(*image.RGBA); ok {
 		*dst = *srcRGBA
 		return
 	}
@@ -156,6 +179,15 @@ func imageToRGBA(dst *image.RGBA, src image.Image) {
 	dst.Stride = 4 * dx
 	dst.Rect = bounds
 
+	if hasCGOConvert {
+		if srcYCbCr, ok := src.(*image.YCbCr); ok &&
+			srcYCbCr.SubsampleRatio == image.YCbCrSubsampleRatio444 {
+			// Use CGO version
+			i444ToRGBACGO(dst, srcYCbCr)
+			return
+		}
+	}
+
 	i := 0
 	for yi := 0; yi < dy; yi++ {
 		for xi := 0; xi < dx; xi++ {
diff --git a/pkg/io/video/convert_cgo.c b/pkg/io/video/convert_cgo.c
new file mode 100644
index 0000000..6b2bc28
--- /dev/null
+++ b/pkg/io/video/convert_cgo.c
@@ -0,0 +1,171 @@
+#include <stdint.h>
+
+#include "_cgo_export.h"
+
+void i444ToI420CGO(
+    unsigned char* cb,
+    unsigned char* cr,
+    const int stride, const int h)
+{
+  int isrc0 = 0;
+  int isrc1 = stride;
+  int idst = 0;
+  for (int y = 0; y < h / 2; y++)
+  {
+    for (int x = 0; x < stride / 2; x++)
+    {
+      const uint8_t cb2 =
+          ((uint16_t)cb[isrc0] + (uint16_t)cb[isrc1] +
+           (uint16_t)cb[isrc0 + 1] + (uint16_t)cb[isrc1 + 1]) /
+          4;
+      const uint8_t cr2 =
+          ((uint16_t)cr[isrc0] + (uint16_t)cr[isrc1] +
+           (uint16_t)cr[isrc0 + 1] + (uint16_t)cr[isrc1 + 1]) /
+          4;
+      cb[idst] = cb2;
+      cr[idst] = cr2;
+      isrc0 += 2;
+      isrc1 += 2;
+      idst++;
+    }
+    isrc0 += stride;
+    isrc1 += stride;
+  }
+}
+
+void i422ToI420CGO(
+    unsigned char* cb,
+    unsigned char* cr,
+    const int stride, const int h)
+{
+  int isrc = 0;
+  int idst = 0;
+  for (int y = 0; y < h / 2; y++)
+  {
+    for (int x = 0; x < stride; x++)
+    {
+      const uint8_t cb2 = ((uint16_t)cb[isrc] + (uint16_t)cb[isrc + stride]) / 2;
+      const uint8_t cr2 = ((uint16_t)cr[isrc] + (uint16_t)cr[isrc + stride]) / 2;
+      cb[idst] = cb2;
+      cr[idst] = cr2;
+      isrc++;
+      idst++;
+    }
+    isrc += stride;
+  }
+}
+
+void rgbToYCbCrCGO(
+    unsigned char* y,
+    unsigned char* cb,
+    unsigned char* cr,
+    const unsigned char r,
+    const unsigned char g,
+    const unsigned char b)
+{
+  // ITU-R BT.601
+  const int y2 = 77 * r + 150 * g + 29 * b;
+  const int cb2 = -43 * r - 85 * g + 128 * b + 0x8000;
+  const int cr2 = 128 * r - 107 * g - 21 * b + 0x8000;
+
+  *y = y2 >> 8;
+  *cb = cb2 >> 8;
+  *cr = cr2 >> 8;
+}
+
+void repeatRGBToYCbCrCGO(
+    const int n,
+    unsigned char* y,
+    unsigned char* cb,
+    unsigned char* cr,
+    const unsigned char r,
+    const unsigned char g,
+    const unsigned char b)
+{
+  int i;
+  for (i = 0; i < n; ++i)
+  {
+    rgbToYCbCrCGO(y, cb, cr, r, g, b);
+  }
+}
+
+void yCbCrToRGBCGO(
+    unsigned char* r,
+    unsigned char* g,
+    unsigned char* b,
+    const unsigned char y,
+    const unsigned char cb,
+    const unsigned char cr)
+{
+  const int cb2 = cb - 0x80;
+  const int cr2 = cr - 0x80;
+
+  // ITU-R BT.601
+  int r2 = 256 * y + 359 * cr2;
+  int g2 = 256 * y - 88 * cb2 - 183 * cr2;
+  int b2 = 256 * y + 454 * cb2;
+
+  if (r2 < 0)
+    r2 = 0;
+  else if (r2 > 0xFFFF)
+    r2 = 0xFFFF;
+  if (g2 < 0)
+    g2 = 0;
+  else if (g2 > 0xFFFF)
+    g2 = 0xFFFF;
+  if (b2 < 0)
+    b2 = 0;
+  else if (b2 > 0xFFFF)
+    b2 = 0xFFFF;
+
+  *r = r2 >> 8;
+  *g = g2 >> 8;
+  *b = b2 >> 8;
+}
+
+void repeatYCbCrToRGBCGO(
+    const int n,
+    unsigned char* r,
+    unsigned char* g,
+    unsigned char* b,
+    const unsigned char y,
+    const unsigned char cb,
+    const unsigned char cr)
+{
+  int i;
+  for (i = 0; i < n; ++i)
+  {
+    yCbCrToRGBCGO(r, g, b, y, cb, cr);
+  }
+}
+
+void i444ToRGBACGO(
+    unsigned char* rgb,
+    const unsigned char* y,
+    const unsigned char* cb,
+    const unsigned char* cr,
+    const int stride, const int h)
+{
+  int i;
+  for (i = 0; i < stride * h; ++i)
+  {
+    yCbCrToRGBCGO(rgb, rgb + 1, rgb + 2, y[i], cb[i], cr[i]);
+    rgb[3] = 0xFF;
+    rgb += 4;
+  }
+}
+
+void rgbaToI444(
+    unsigned char* y,
+    unsigned char* cb,
+    unsigned char* cr,
+    const unsigned char* rgb,
+    const int stride, const int h)
+{
+  int i;
+  for (i = 0; i < stride * h; ++i)
+  {
+    rgbToYCbCrCGO(&y[i], &cb[i], &cr[i], rgb[0], rgb[1], rgb[2]);
+    rgb += 4;
+  }
+}
diff --git a/pkg/io/video/convert_cgo.go b/pkg/io/video/convert_cgo.go
new file mode 100644
index 0000000..eb96468
--- /dev/null
+++ b/pkg/io/video/convert_cgo.go
@@ -0,0 +1,89 @@
+// +build cgo
+
+package video
+
+import (
+	"image"
+)
+
+// #include "convert_cgo.h"
+import "C"
+
+func init() {
+	hasCGOConvert = true
+}
+
+func i444ToI420CGO(img *image.YCbCr) {
+	h := img.Rect.Dy()
+	C.i444ToI420CGO(
+		(*C.uchar)(&img.Cb[0]), (*C.uchar)(&img.Cr[0]),
+		C.int(img.CStride), C.int(h),
+	)
+	img.CStride = img.CStride / 2
+	cLen := img.CStride * (h / 2)
+	img.Cb = img.Cb[:cLen]
+	img.Cr = img.Cr[:cLen]
+}
+
+func i422ToI420CGO(img *image.YCbCr) {
+	h := img.Rect.Dy()
+	C.i422ToI420CGO(
+		(*C.uchar)(&img.Cb[0]), (*C.uchar)(&img.Cr[0]),
+		C.int(img.CStride), C.int(h),
+	)
+	cLen := img.CStride * (h / 2)
+	img.Cb = img.Cb[:cLen]
+	img.Cr = img.Cr[:cLen]
+}
+
+func rgbToYCbCrCGO(y, cb, cr *uint8, r, g, b uint8) { // For testing
+	C.rgbToYCbCrCGO(
+		(*C.uchar)(y), (*C.uchar)(cb), (*C.uchar)(cr),
+		C.uchar(r), C.uchar(g), C.uchar(b),
+	)
+}
+
+func repeatRGBToYCbCrCGO(n int, y, cb, cr *uint8, r, g, b uint8) { // For testing
+	C.repeatRGBToYCbCrCGO(
+		C.int(n),
+		(*C.uchar)(y), (*C.uchar)(cb), (*C.uchar)(cr),
+		C.uchar(r), C.uchar(g), C.uchar(b),
+	)
+}
+
+func yCbCrToRGBCGO(r, g, b *uint8, y, cb, cr uint8) { // For testing
+	C.yCbCrToRGBCGO(
+		(*C.uchar)(r), (*C.uchar)(g), (*C.uchar)(b),
+		C.uchar(y), C.uchar(cb), C.uchar(cr),
+	)
+}
+
+func repeatYCbCrToRGBCGO(n int, r, g, b *uint8, y, cb, cr uint8) { // For testing
+	C.repeatYCbCrToRGBCGO(
+		C.int(n),
+		(*C.uchar)(r), (*C.uchar)(g), (*C.uchar)(b),
+		C.uchar(y), C.uchar(cb), C.uchar(cr),
+	)
+}
+
+func i444ToRGBACGO(dst *image.RGBA, src *image.YCbCr) {
+	C.i444ToRGBACGO(
+		(*C.uchar)(&dst.Pix[0]),
+		(*C.uchar)(&src.Y[0]),
+		(*C.uchar)(&src.Cb[0]),
+		(*C.uchar)(&src.Cr[0]),
+		C.int(src.Rect.Dx()),
+		C.int(src.Rect.Dy()),
+	)
+}
+
+func rgbaToI444(dst *image.YCbCr, src *image.RGBA) {
+	C.rgbaToI444(
+		(*C.uchar)(&dst.Y[0]),
+		(*C.uchar)(&dst.Cb[0]),
+		(*C.uchar)(&dst.Cr[0]),
+		(*C.uchar)(&src.Pix[0]),
+		C.int(src.Rect.Dx()),
+		C.int(src.Rect.Dy()),
+	)
+}
diff --git a/pkg/io/video/convert_cgo.h b/pkg/io/video/convert_cgo.h
new file mode 100644
index 0000000..ab60e77
--- /dev/null
+++ b/pkg/io/video/convert_cgo.h
@@ -0,0 +1,57 @@
+void i444ToI420CGO(
+    unsigned char* cb,
+    unsigned char* cr,
+    const int stride, const int h);
+
+void i422ToI420CGO(
+    unsigned char* cb,
+    unsigned char* cr,
+    const int stride, const int h);
+
+void rgbToYCbCrCGO(
+    unsigned char* y,
+    unsigned char* cb,
+    unsigned char* cr,
+    const unsigned char r,
+    const unsigned char g,
+    const unsigned char b);  // for testing
+
+void repeatRGBToYCbCrCGO(
+    const int n,
+    unsigned char* y,
+    unsigned char* cb,
+    unsigned char* cr,
+    const unsigned char r,
+    const unsigned char g,
+    const unsigned char b);  // for testing
+
+void yCbCrToRGBCGO(
+    unsigned char* r,
+    unsigned char* g,
+    unsigned char* b,
+    const unsigned char y,
+    const unsigned char cb,
+    const unsigned char cr);  // for testing
+
+void repeatYCbCrToRGBCGO(
+    const int n,
+    unsigned char* r,
+    unsigned char* g,
+    unsigned char* b,
+    const unsigned char y,
+    const unsigned char cb,
+    const unsigned char cr);  // for testing
+
+void i444ToRGBACGO(
+    unsigned char* rgb,
+    const unsigned char* y,
+    const unsigned char* cb,
+    const unsigned char* cr,
+    const int stride, const int h);
+
+void rgbaToI444(
+    unsigned char* y,
+    unsigned char* cb,
+    unsigned char* cr,
+    const unsigned char* rgb,
+    const int stride, const int h);
diff --git a/pkg/io/video/scale.go b/pkg/io/video/scale.go
index a7a40d6..89f6117 100644
--- a/pkg/io/video/scale.go
+++ b/pkg/io/video/scale.go
@@ -95,6 +95,8 @@ func Scale(width, height int, scaler Scaler) TransformFunc {
 		// ycbcrRealloc reallocs image.YCbCr if needed
 		ycbcrRealloc := func(i1 *image.YCbCr) {
 			if imgScaled == nil || imgScaled.ColorModel() != i1.ColorModel() {
+				updateRect(i1.Rect)
+				cacheScaler(rect, i1.Rect)
 				imgScaled = image.NewYCbCr(rect, i1.SubsampleRatio)
 			}
 			imgDst := imgScaled.(*image.YCbCr)
@@ -135,6 +137,8 @@ func Scale(width, height int, scaler Scaler) TransformFunc {
 		// ycbcrRealloc reallocs image.RGBA if needed
 		rgbaRealloc := func(i1 *image.RGBA) {
 			if imgScaled == nil || imgScaled.ColorModel() != i1.ColorModel() {
+				updateRect(i1.Rect)
+				cacheScaler(rect, i1.Rect)
 				imgScaled = image.NewRGBA(rect)
 			}
 			imgDst := imgScaled.(*image.RGBA)


Test patch:
diff --git a/pkg/driver/videotest/dummy.go b/pkg/driver/videotest/dummy.go
index 7f4eb7c..c0a1c2f 100644
--- a/pkg/driver/videotest/dummy.go
+++ b/pkg/driver/videotest/dummy.go
@@ -47,12 +47,10 @@ func (d *dummy) Close() error {
 }
 
 func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) {
-	yi := p.Width * p.Height
-	ci := yi / 2
+	if p.FrameRate == 0 {
+		p.FrameRate = 30
+	}
 
-	yy := make([]byte, yi)
-	cb := make([]byte, ci)
-	cr := make([]byte, ci)
 	colors := [][3]byte{
 		{235, 128, 128},
 		{210, 16, 146},
@@ -63,9 +61,44 @@ func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) {
 		{41, 240, 110},
 	}
 
-	if p.FrameRate == 0 {
-		p.FrameRate = 30
+	yi := p.Width * p.Height
+	ci := yi / 2
+	yy := make([]byte, yi)
+	cb := make([]byte, ci)
+	cr := make([]byte, ci)
+	yyBase := make([]byte, yi)
+	cbBase := make([]byte, ci)
+	crBase := make([]byte, ci)
+	hColorBarEnd := p.Height * 3 / 4
+	wGradationEnd := p.Width * 5 / 7
+	for y := 0; y < hColorBarEnd; y++ {
+		yi := p.Width * y
+		ci := p.Width * y / 2
+		// Color bar
+		for x := 0; x < p.Width; x++ {
+			c := x * 7 / p.Width
+			yyBase[yi+x] = uint8(uint16(colors[c][0]) * 75 / 100)
+			cbBase[ci+x/2] = colors[c][1]
+			crBase[ci+x/2] = colors[c][2]
+		}
 	}
+	for y := hColorBarEnd; y < p.Height; y++ {
+		yi := p.Width * y
+		ci := p.Width * y / 2
+		for x := 0; x < wGradationEnd; x++ {
+			// Gray gradation
+			yyBase[yi+x] = uint8(x * 255 / wGradationEnd)
+			cbBase[ci+x/2] = 128
+			crBase[ci+x/2] = 128
+		}
+		for x := wGradationEnd; x < p.Width; x++ {
+			// Noise area
+			cbBase[ci+x/2] = 128
+			crBase[ci+x/2] = 128
+		}
+	}
+	random := rand.New(rand.NewSource(0))
+
 	d.tick = time.NewTicker(time.Duration(float32(time.Second) / p.FrameRate))
 
 	r := video.ReaderFunc(func() (image.Image, error) {
@@ -77,32 +110,14 @@ func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) {
 
 		<-d.tick.C
 
-		for y := 0; y < p.Height; y++ {
+		copy(yy, yyBase)
+		copy(cb, cbBase)
+		copy(cr, crBase)
+		for y := hColorBarEnd; y < p.Height; y++ {
 			yi := p.Width * y
-			ci := p.Width * y / 2
-			if y > p.Height*3/4 {
-				for x := 0; x < p.Width; x++ {
-					c := x * 7 / p.Width
-					if c > 4 {
-						// Noise
-						yy[yi+x] = uint8(rand.Int31n(2) * 255)
-						cb[ci+x/2] = 128
-						cr[ci+x/2] = 128
-					} else {
-						// Gray
-						yy[yi+x] = uint8(x * 255 * 7 / (5 * p.Width))
-						cb[ci+x/2] = 128
-						cr[ci+x/2] = 128
-					}
-				}
-			} else {
-				// Color bar
-				for x := 0; x < p.Width; x++ {
-					c := x * 7 / p.Width
-					yy[yi+x] = uint8(uint16(colors[c][0]) * 75 / 100)
-					cb[ci+x/2] = colors[c][1]
-					cr[ci+x/2] = colors[c][2]
-				}
+			for x := wGradationEnd; x < p.Width; x++ {
+				// Noise
+				yy[yi+x] = uint8(random.Int31n(2) * 255)
 			}
 		}
 		return &image.YCbCr{
diff --git a/pkg/io/video/convert_cgo_test.go b/pkg/io/video/convert_cgo_test.go
new file mode 100644
index 0000000..82dec18
--- /dev/null
+++ b/pkg/io/video/convert_cgo_test.go
@@ -0,0 +1,84 @@
+// +build cgo
+
+package video
+
+import (
+	"image/color"
+	"testing"
+)
+
+func diffUint8(a, b uint8) uint8 {
+	d := int(a) - int(b)
+	if d < 0 {
+		d = -d
+	}
+	return uint8(d)
+}
+
+func TestRGBToYCbCr(t *testing.T) {
+	for r := 0; r < 0x100; r += 8 {
+		for g := 0; g < 0x100; g += 8 {
+			for b := 0; b < 0x100; b += 8 {
+				var y, cb, cr uint8
+				rgbToYCbCrCGO(&y, &cb, &cr, uint8(r), uint8(g), uint8(b))
+				y2, cb2, cr2 := color.RGBToYCbCr(uint8(r), uint8(g), uint8(b))
+				if diffUint8(y, y2) > 2 || diffUint8(cb, cb2) > 2 || diffUint8(cr, cr2) > 2 {
+					t.Fatalf(
+						"rgbToYCbCrCGO differs from color.RGBToYCbCr: in(%d, %d, %d), "+
+							"expected(%d, %d, %d), got(%d, %d, %d)",
+						r, g, b,
+						y2, cb2, cr2,
+						y, cb, cr,
+					)
+				}
+			}
+		}
+	}
+}
+
+func TestYCbCrToRGB(t *testing.T) {
+	for y := 0; y < 0x100; y += 8 {
+		for cb := 0; cb < 0x100; cb += 8 {
+			for cr := 0; cr < 0x100; cr += 8 {
+				var r, g, b uint8
+				yCbCrToRGBCGO(&r, &g, &b, uint8(y), uint8(cb), uint8(cr))
+				r2, g2, b2 := color.YCbCrToRGB(uint8(y), uint8(cb), uint8(cr))
+				if diffUint8(r, r2) > 2 || diffUint8(g, g2) > 2 || diffUint8(b, b2) > 2 {
+					t.Fatalf(
+						"yCbCrToRGBCGO differs from color.YCbCrToRGB: in(%d, %d, %d), "+
+							"expected(%d, %d, %d), got(%d, %d, %d)",
+						y, cb, cr,
+						r2, g2, b2,
+						r, g, b,
+					)
+				}
+			}
+		}
+	}
+}
+
+func BenchmarkRGBToYCbCr(b *testing.B) {
+	b.Run("Go", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			_, _, _ = color.RGBToYCbCr(200, 100, 10)
+		}
+	})
+	b.Run("CGO", func(b *testing.B) {
+		var y, cb, cr uint8
+		// Loop in C code To ignore CGO call overhead
+		repeatRGBToYCbCrCGO(b.N, &y, &cb, &cr, 200, 100, 10)
+	})
+}
+
+func BenchmarkYCbCrToRGB(b *testing.B) {
+	b.Run("Go", func(b *testing.B) {
+		for i := 0; i < b.N; i++ {
+			_, _, _ = color.YCbCrToRGB(200, 100, 10)
+		}
+	})
+	b.Run("CGO", func(b *testing.B) {
+		var rr, gg, bb uint8
+		// Loop in C code To ignore CGO call overhead
+		repeatYCbCrToRGBCGO(b.N, &rr, &gg, &bb, 200, 100, 10)
+	})
+}
diff --git a/pkg/io/video/convert_test.go b/pkg/io/video/convert_test.go
index 28f782f..aeede78 100644
--- a/pkg/io/video/convert_test.go
+++ b/pkg/io/video/convert_test.go
@@ -108,6 +108,38 @@ func TestToI420(t *testing.T) {
 				Rect:    image.Rect(0, 0, 4, 4),
 			},
 		},
+		"RGBA": {
+			src: &image.RGBA{
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+					0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+					0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+					0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+				},
+				Stride: 16,
+				Rect:   image.Rect(0, 0, 4, 4),
+			},
+			expected: &image.YCbCr{
+				SubsampleRatio: image.YCbCrSubsampleRatio420,
+				Y: []uint8{
+					0x00, 0x00, 0x00, 0x00,
+					0x00, 0x00, 0x00, 0x00,
+					0xFF, 0xFF, 0x00, 0x00,
+					0xFF, 0xFF, 0x00, 0x00,
+				},
+				Cb: []uint8{
+					0x80, 0x80,
+					0x80, 0x80,
+				},
+				Cr: []uint8{
+					0x80, 0x80,
+					0x80, 0x80,
+				},
+				YStride: 4,
+				CStride: 2,
+				Rect:    image.Rect(0, 0, 4, 4),
+			},
+		},
 	}
 	for name, c := range cases {
 		c := c
@@ -126,12 +158,78 @@ func TestToI420(t *testing.T) {
 	}
 }
 
+func TestToRGBA(t *testing.T) {
+	cases := map[string]struct {
+		src      image.Image
+		expected image.Image
+	}{
+		"I444": {
+			src: &image.YCbCr{
+				SubsampleRatio: image.YCbCrSubsampleRatio420,
+				Y: []uint8{
+					0x00, 0x00, 0x00, 0x00,
+					0x00, 0x00, 0x00, 0x00,
+					0xFF, 0xFF, 0x00, 0x00,
+					0xFF, 0xFF, 0x00, 0x00,
+				},
+				Cb: []uint8{
+					0x80, 0x80,
+					0x80, 0x80,
+				},
+				Cr: []uint8{
+					0x80, 0x80,
+					0x80, 0x80,
+				},
+				YStride: 4,
+				CStride: 2,
+				Rect:    image.Rect(0, 0, 4, 4),
+			},
+			expected: &image.RGBA{
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+					0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+					0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+					0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
+				},
+				Stride: 16,
+				Rect:   image.Rect(0, 0, 4, 4),
+			},
+		},
+	}
+	for name, c := range cases {
+		c := c
+		t.Run(name, func(t *testing.T) {
+			r := ToRGBA(ReaderFunc(func() (image.Image, error) {
+				return c.src, nil
+			}))
+			out, err := r.Read()
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+			if !reflect.DeepEqual(c.expected, out) {
+				t.Errorf("Expected output image:\n%v\ngot:\n%v", c.expected, out)
+			}
+		})
+	}
+}
+
+func TestNoCGO(t *testing.T) {
+	if !hasCGOConvert {
+		t.SkipNow()
+	}
+	hasCGOConvert = false
+	t.Run("ToI420", TestToI420)
+	t.Run("ToRGBA", TestToRGBA)
+	hasCGOConvert = true
+}
+
 func BenchmarkToI420(b *testing.B) {
 	for name, sz := range imageSizes {
 		cases := map[string]image.Image{
 			"I444": image.NewYCbCr(image.Rect(0, 0, sz[0], sz[1]), image.YCbCrSubsampleRatio444),
 			"I422": image.NewYCbCr(image.Rect(0, 0, sz[0], sz[1]), image.YCbCrSubsampleRatio422),
 			"I420": image.NewYCbCr(image.Rect(0, 0, sz[0], sz[1]), image.YCbCrSubsampleRatio420),
+			"RGBA": image.NewRGBA(image.Rect(0, 0, sz[0], sz[1])),
 		}
 		b.Run(name, func(b *testing.B) {
 			for name, img := range cases {
@@ -152,3 +250,41 @@ func BenchmarkToI420(b *testing.B) {
 		})
 	}
 }
+
+func BenchmarkToRGBA(b *testing.B) {
+	for name, sz := range imageSizes {
+		cases := map[string]image.Image{
+			"I444": image.NewYCbCr(image.Rect(0, 0, sz[0], sz[1]), image.YCbCrSubsampleRatio444),
+			"I422": image.NewYCbCr(image.Rect(0, 0, sz[0], sz[1]), image.YCbCrSubsampleRatio422),
+			"I420": image.NewYCbCr(image.Rect(0, 0, sz[0], sz[1]), image.YCbCrSubsampleRatio420),
+			"RGBA": image.NewRGBA(image.Rect(0, 0, sz[0], sz[1])),
+		}
+		b.Run(name, func(b *testing.B) {
+			for name, img := range cases {
+				img := img
+				b.Run(name, func(b *testing.B) {
+					r := ToRGBA(ReaderFunc(func() (image.Image, error) {
+						return img, nil
+					}))
+
+					for i := 0; i < b.N; i++ {
+						_, err := r.Read()
+						if err != nil {
+							b.Fatalf("Unexpected error: %v", err)
+						}
+					}
+				})
+			}
+		})
+	}
+}
+
+func BenchmarkNoCGO(b *testing.B) {
+	if !hasCGOConvert {
+		b.SkipNow()
+	}
+	hasCGOConvert = false
+	b.Run("ToI420", BenchmarkToI420)
+	b.Run("ToRGBA", BenchmarkToRGBA)
+	hasCGOConvert = true
+}