Attached is a patch to make the 32 to 32 blit with alpha on both src & dest do accurate compositing. Also attached is a unit test to demonstrate the current error & confirm the fix. Also attached is a script that can be used to performance test the patch.
Performance tests on my P3 700 show that in the worst case (where every pixel from the source is translucent and all dest pixels have non-zero alpha) the accurate stuff can take nearly twice as long (90% longer). However in the most common cases, the approaches are comparable. When the dest is empty to start, the new stuff takes about 5% longer. When most source pixels are clear or opaque, then new blend can take up to 10% less time (both those differences are because of some extra early skip checks).
Attachment:
AccurateAlphaBlending.patch
Description: Binary data
import pygame import unittest class UnitTestAlphaBlitCompositing(unittest.TestCase): def assertMaxSurfaceDifference(self, surf_a, surf_b, max_diff): self.assertEqual(surf_a.get_width(), surf_b.get_width()) self.assertEqual(surf_a.get_height(), surf_b.get_height()) for y in xrange(surf_a.get_height()): for x in xrange(surf_a.get_width()): color_a = surf_a.get_at((x,y)) color_b = surf_b.get_at((x,y)) if color_a[3] != 0 or color_b[3] != 0: for i in xrange(4): diff = abs(color_a[i] - color_b[i]) if (diff > max_diff): print "max diff exceeded at:", x, y, "with", color_a, "vs.", color_b self.assert_(diff <= max_diff) def testCompositing(self): size = (81,18) # first we get 2 source images, that have a variety of alpha values # over a range of color values that have different sets # intersecting color values and unique color values, # making sure to also hit the extremes (0 & 255) src_a = pygame.surface.Surface(size, pygame.SRCALPHA, 32) src_b = pygame.surface.Surface(size, pygame.SRCALPHA, 32) for color_index in xrange(81): for alpha_index in xrange(18): color_1_value = min(255, (color_index / 9)*32) color_2_value = min(255, (color_index % 9)*32) alpha_value = alpha_index*15 pos = (color_index, alpha_index) color_a = (color_1_value, color_2_value, 0, alpha_value) src_a.set_at(pos, color_a) color_b = (0, color_1_value, color_2_value, alpha_value) src_b.set_at(pos, color_b) # see what you get when blitting b, then blitting a blit_in_turn_dest = pygame.surface.Surface(size, 0, 24) blit_in_turn_dest.blit(src_b, (0,0)) blit_in_turn_dest.blit(src_a, (0,0)) # see what you get when making a composite of b over a # and then blit that composite_blit_dest = pygame.surface.Surface(size, 0, 24) composite_surface = pygame.surface.Surface(size, pygame.SRCALPHA, 32) composite_surface.blit(src_b, (0,0)) self.assertMaxSurfaceDifference(composite_surface, src_b, 0) composite_surface.blit(src_a, (0,0)) #self.slowButAccurateCompositeAlphaBlit(src_a, composite_surface) composite_blit_dest.blit(composite_surface, (0,0)) # finally we compare the surfaces # the composite cannot be perfect due to rounding, so we test within # some limit human eyes probably can't see much self.assertMaxSurfaceDifference(composite_blit_dest, blit_in_turn_dest, 4) unittest.main()
import pygame import time def MakeSource(size, ratio_empty, ratio_transparent): surf = pygame.surface.Surface(size, pygame.SRCALPHA, 32) for x in xrange(size[0]): if x < size[0]*ratio_empty: color = (0,0,0,0) elif x < size[0]*(ratio_empty + ratio_transparent): color = (128,128,128,128) else: color = (255,255,255,255) for y in xrange(size[1]): surf.set_at((x,y), color) set_color = surf.get_at((x,y)) assert(set_color == color) return surf def MakeDest(size, ratio_empty, ratio_transparent): surf = pygame.surface.Surface(size, pygame.SRCALPHA, 32) for y in xrange(size[1]): if y < size[1]*ratio_empty: color = (0,0,0,0) elif y < size[1]*(ratio_empty + ratio_transparent): color = (128,128,128,128) else: color = (255,255,255,255) for x in xrange(size[0]): surf.set_at((x,y), color) set_color = surf.get_at((x,y)) assert(set_color == color) return surf def TimeCompositing(source, dest, runs): elapsed = 0 for i in xrange(runs): test_dest = dest.copy() start = time.clock() test_dest.blit(source, (0,0)) end = time.clock() elapsed += end - start return elapsed sizes = [[256,256], [800,600]] source_ratios = [["average", .3,.1], ["fully transparent",0,1]] dest_ratios = [["average", .3,.1], ["empty", 1,0], ["opaque", 0, 0]] runs = 10 sets = 3 print "testing",runs,"runs..." for size in sizes: for src_ratio in source_ratios: for dest_ratio in dest_ratios: times = [] for set in xrange(sets): source = MakeSource(size, src_ratio[1], src_ratio[2]) dest = MakeDest(size, dest_ratio[1], dest_ratio[2]) times.append(TimeCompositing(source, dest, runs)) print times, ' for size ', size,'and',src_ratio[0],'src with', dest_ratio[0], 'dest'