OXYGENHUB
◆ main openmsx / src / video / scalers / hq.py
13.9 KiB · Python · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
# To run:
2
#   python3 hq.py
3
#
4
# To profile:
5
#   python3 -m cProfile -s cumulative hq.py > hq-profile.txt
6
7
from hq_gen import (
8
	edges, expandQuadrant, genExpr2, genExpr3, genExpr4,
9
	permuteCase, simplifyWeights
10
	)
11
12
from collections import defaultdict
13
from itertools import count
14
from math import sqrt
15
import sys
16
17
# Variant classes:
18
19
class Variant(object):
20
21
	def __init__(self, pixelExpr, lite, table, narrow = None):
22
		self.lite = lite
23
		self.table = table
24
		if lite:
25
			pixelExpr = makeLite(pixelExpr, table)
26
		if narrow is not None:
27
			pixelExpr = narrow(pixelExpr)
28
                #   0 | 1 | 2
29
                #  ---+---+---
30
                #   3 | 4 | 5
31
                #  ---+---+---
32
                #   6 | 7 | 8
33
                # For the openGL shaders we want the following order:
34
                #   (4,6), (3,7), (3,4), (1,3), (0,4), (4,7), (1,4), (5,7), (4,8), (4,5), (2,4), (1,5)
35
                #   see comments in src/video/scalers/HQCommon.hh for why this order was chosen.
36
                # Compare this to the original order in 'edges' in hq_gen.py.
37
                # To switch between these different orderings we use the following permutations.
38
		pixelExpr = permuteCases(
39
			(9, 2, 7, 3, 4, 10, 5, 1, 11, 8, 6, 0)  # openGL
40
			if table else
41
			(2, 9, 7, 4, 3, 10, 11, 1, 8, 0, 6, 5),
42
			pixelExpr
43
			)
44
		self.pixelExpr = pixelExpr
45
46
	def writeSwitch(self, fileName):
47
		writeTextFile(fileName, genSwitch(self.pixelExpr))
48
49
# I/O:
50
51
def printText(contents):
52
	for text in contents:
53
		sys.stdout.write(text)
54
55
def writeTextFile(fileName, contents):
56
	with open(fileName, 'w') as out:
57
		for content in contents:
58
			out.write(content)
59
60
def writeBinaryFile(fileName, data):
61
	with open(fileName, 'wb') as out:
62
		out.write(bytes(data))
63
64
# Output as switch statement:
65
66
def genSwitch(pixelExpr):
67
	# Note: In practice, only the center subpixel of HQ3x is independent of
68
	#       the edge bits. So maybe this is a bit overengineered, but it does
69
	#       express why this optimization is correct.
70
	subExprForSubPixel = tuple(
71
		{expr[subPixel] for expr in pixelExpr if expr[subPixel] is not None}
72
		for subPixel in range(len(pixelExpr[0]))
73
		)
74
	isIndependentSubPixel = tuple(
75
		len(subExprs) == 1
76
		for subExprs in subExprForSubPixel
77
		)
78
79
	exprToCases = defaultdict(list)
80
	for case, expr in enumerate(pixelExpr):
81
		if expr[0] is None:
82
			assert all(subExpr is None for subExpr in expr)
83
			key = None
84
		else:
85
			key = tuple(tuple(subExpr) for subExpr in expr)
86
		exprToCases[key].append(case)
87
	yield 'switch (pattern) {\n'
88
	for cases, expr in sorted(
89
		( sorted(cases), expr )
90
		for expr, cases in exprToCases.items()
91
		):
92
		if expr is not None:
93
			for case in cases:
94
				yield 'case %d:\n' % case
95
			for subPixel, subExpr in enumerate(expr):
96
				if not isIndependentSubPixel[subPixel]:
97
					yield '\tpixel%s = %s;\n' % (
98
						hex(subPixel)[2 : ], getBlendCode(subExpr)
99
						)
100
			yield '\tbreak;\n'
101
	for case in sorted(exprToCases[None]):
102
		yield 'case %d:\n' % case
103
	yield 'default:\n'
104
	yield '\tUNREACHABLE;\n'
105
	yield '\t%s = 0; // avoid warning\n' % ' = '.join(
106
		'pixel%d' % subPixel
107
		for subPixel, independent_ in enumerate(isIndependentSubPixel)
108
		)
109
	yield '}\n'
110
111
	for subPixel, independent in enumerate(isIndependentSubPixel):
112
		if independent:
113
			subExpr, = subExprForSubPixel[subPixel]
114
			yield 'pixel%s = %s;\n' % (
115
				hex(subPixel)[2 : ], getBlendCode(subExpr)
116
				)
117
118
def getBlendCode(weights):
119
	wSum = sum(weights)
120
	assert isPow2(wSum)
121
	if wSum == 1:
122
		index, = (index for index, weight in enumerate(weights) if weight != 0)
123
		return 'c%d' % (index + 1)
124
	elif wSum <= 8:
125
		# Because the lower 3 bits of each color component (R,G,B)
126
		# are zeroed out, we can operate on a single integer as if it
127
		# is a vector.
128
		return ' + '.join(
129
			'(c%d / %d) * %d' % (index + 1, wSum, weight)
130
			for index, weight in enumerate(weights)
131
			if weight != 0
132
			)
133
	else:
134
		return '((%s) & 0xFF00FF00) | (((%s) / %d) & 0x00FF00FF)' % (
135
			' + '.join(
136
				'((c%d & 0xFF00FF00) / %d) * %d' % (index + 1, wSum, weight)
137
				for index, weight in enumerate(weights)
138
				if weight != 0
139
				),
140
			' + '.join(
141
				'(c%d & 0x00FF00FF) * %d' % (index + 1, weight)
142
				for index, weight in enumerate(weights)
143
				if weight != 0
144
				),
145
			wSum
146
			)
147
148
# Table output as text:
149
150
def formatOffsetsTable(pixelExpr):
151
	for case, expr in enumerate(pixelExpr):
152
		yield '// %d\n' % case
153
		for weights in expr:
154
			for x, y in transformOffsets(weights):
155
				yield ' %3d, %3d,' % (x, y)
156
			yield '\n'
157
158
def formatWeightsTable(pixelExpr, cellFunc):
159
	for case, expr in enumerate(pixelExpr):
160
		yield '// %d\n' % case
161
		for weights in expr:
162
			for weight in transformWeights(weights, cellFunc):
163
				yield ' %3d,' % weight
164
			yield '\n'
165
166
# The rest:
167
168
def getZoom(pixelExpr):
169
	zoom = int(sqrt(len(pixelExpr[0])))
170
	assert zoom * zoom == len(pixelExpr[0])
171
	return zoom
172
173
def isPow2(num):
174
	while num & 1 == 0:
175
		num >>= 1
176
	return num == 1
177
178
def permuteCases(permutation, pixelExpr):
179
	return [
180
		pixelExpr[permuteCase(case, permutation)]
181
		for case in range(len(pixelExpr))
182
		]
183
184
def computeNeighbours(weights):
185
	neighbours = [ i for i in range(9) if i != 4 and weights[i] != 0 ]
186
	assert len(neighbours) <= 2
187
	neighbours += [ None ] * (2 - len(neighbours))
188
	return neighbours
189
190
def transformOffsets(weights):
191
	for neighbour in computeNeighbours(weights):
192
		yield (
193
			min(255, (1 if neighbour is None else neighbour % 3) * 128),
194
			min(255, (1 if neighbour is None else neighbour // 3) * 128)
195
			)
196
197
def transformWeights(weights, cellFunc):
198
	scaledWeights = scaleWeights(weights, 256)
199
	for cell in cellFunc(scaledWeights):
200
		yield min(255, 0 if cell is None else scaledWeights[cell])
201
202
def computeOffsets(pixelExpr):
203
	'''Computes offsets for the fragment shader.
204
	Output is a 64N * 64N texture, where N is the zoom factor.
205
	'''
206
	zoom = getZoom(pixelExpr)
207
	for caseMajor in range(0, len(pixelExpr), 64):
208
		for subY in range(0, zoom ** 2, zoom):
209
			for caseMinor in range(64):
210
				for subX in range(zoom):
211
					weights = pixelExpr[caseMajor + caseMinor][subY + subX]
212
					for x, y in transformOffsets(weights):
213
						yield x
214
						yield y
215
216
def computeWeights(pixelExpr, cellFunc):
217
	'''Computes weights for the fragment shader.
218
	Output is a 64N * 64N texture, where N is the zoom factor.
219
	'''
220
	zoom = getZoom(pixelExpr)
221
	for caseMajor in range(0, len(pixelExpr), 64):
222
		for subY in range(0, zoom ** 2, zoom):
223
			for caseMinor in range(64):
224
				for subX in range(zoom):
225
					weights = pixelExpr[caseMajor + caseMinor][subY + subX]
226
					for tw in transformWeights(weights, cellFunc):
227
						yield tw
228
229
def computeWeightCells(weights):
230
	neighbours = computeNeighbours(weights)
231
	return (neighbours[0], neighbours[1], 4)
232
233
def computeLiteWeightCells(weights_):
234
	return (3, 4, 5)
235
236
def isContradiction(case):
237
	inv = case ^ 0xFFF
238
	return (
239
		(inv & 0x121) in [0x120, 0x101, 0x021] or
240
		(inv & 0x502) in [0x500, 0x402, 0x102] or
241
		(inv & 0x484) in [0x480, 0x404, 0x084] or
242
		(inv & 0x0A8) in [0x0A0, 0x088, 0x028] or
243
		(inv & 0x00F) in [0x00E, 0x00D, 0x00B, 0x007]
244
		)
245
246
def scaleWeights(weights, newSum):
247
	'''Returns the given weights, scaled such that their sum is the given one.
248
	'''
249
	oldSum = sum(weights)
250
	factor, remainder = divmod(newSum, oldSum)
251
	assert remainder == 0
252
	return tuple(weight * factor for weight in weights)
253
254
def blendWeights(weights1, weights2, factor1 = 1, factor2 = 1):
255
	factor1 *= sum(weights2)
256
	factor2 *= sum(weights1)
257
	return simplifyWeights(
258
		factor1 * w1 + factor2 * w2
259
		for w1, w2 in zip(weights1, weights2)
260
		)
261
262
def calcNeighbourToSet():
263
	# Compute equivalence classes.
264
	ret = []
265
	for case in range(1 << len(edges)):
266
		if isContradiction(case):
267
			ret.append(None)
268
		else:
269
			neighbourToSet = [{neighbour} for neighbour in range(9)]
270
			for edge, (neighbour1, neighbour2) in enumerate(edges):
271
				if (case & (1 << edge)) == 0:
272
					# No edge, so the two pixels are equal.
273
					# Merge the sets of equal pixels.
274
					set1 = neighbourToSet[neighbour1]
275
					set2 = neighbourToSet[neighbour2]
276
					set1 |= set2
277
					for n in set2:
278
						neighbourToSet[n] = set1
279
			ret.append(neighbourToSet)
280
	return ret
281
282
neighbourToSet = calcNeighbourToSet()
283
284
def lighten(case, weights, neighbourPreference):
285
	equalNeighboursOf = neighbourToSet[case]
286
	if equalNeighboursOf is None:
287
		return None
288
	else:
289
		done = set()
290
		newWeights = [ 0 ] * 9
291
		for neighbour in neighbourPreference:
292
			if neighbour not in done:
293
				equalNeighbours = equalNeighboursOf[neighbour]
294
				newWeights[neighbour] = sum(
295
					weights[n] for n in equalNeighbours
296
					)
297
				done |= equalNeighbours
298
		for c in (0, 1, 2, 6, 7, 8):
299
			# Only c4, c5, c6 have non-zero weight.
300
			assert newWeights[c] == 0
301
		return simplifyWeights(newWeights)
302
303
def makeLite(pixelExpr, biased):
304
	zoom = getZoom(pixelExpr)
305
	biasLeft  = (4, 3, 5, 1, 7, 0, 2, 6, 8)
306
	biasRight = (4, 5, 3, 1, 7, 2, 0, 8, 6)
307
	if biased:
308
		center = (zoom - 1) / 2
309
		neighbourPreferences = [
310
			biasRight if subPixel % zoom > center else biasLeft
311
			for subPixel in range(zoom ** 2)
312
			]
313
	else:
314
		neighbourPreferences = [ biasLeft ] * (zoom ** 2)
315
	return [
316
		[ lighten(case, weights, pref)
317
		  for weights, pref in zip(expr, neighbourPreferences) ]
318
		for case, expr in enumerate(pixelExpr)
319
		]
320
321
def makeNarrow2to1(pixelExpr):
322
	return tuple(
323
		(None, None) if a is None else (blendWeights(a, b), blendWeights(c, d))
324
		for a, b, c, d in pixelExpr
325
		)
326
327
def makeNarrow3to2(pixelExpr):
328
	return [
329
		[	blendWeights(expr[0], expr[1], 2, 1),
330
			blendWeights(expr[2], expr[1], 2, 1),
331
			blendWeights(expr[3], expr[4], 2, 1),
332
			blendWeights(expr[5], expr[4], 2, 1),
333
			blendWeights(expr[6], expr[7], 2, 1),
334
			blendWeights(expr[8], expr[7], 2, 1),
335
			]
336
		for expr in pixelExpr
337
		]
338
339
def genHQLiteOffsetsTable(pixelExpr):
340
	'''In the hqlite case, the result color depends on at most one neighbour
341
	color. Therefore, an offset into an interpolated texture is used instead
342
	of explicit weights.
343
	Output is a 64N * 64N texture, where N is the zoom factor.
344
	'''
345
	zoom = getZoom(pixelExpr)
346
	for caseMajor in range(0, len(pixelExpr), 64):
347
		for subY in range(zoom):
348
			for caseMinor in range(64):
349
				for subX in range(zoom):
350
					subPixel = zoom * subY + subX
351
					weights = pixelExpr[caseMajor + caseMinor][subPixel]
352
					if weights is None:
353
						neighbour = None
354
					else:
355
						neighbours = computeNeighbours(weights)
356
						assert neighbours[1] is None, neighbours
357
						neighbour = neighbours[0]
358
						factor = sum(weights)
359
360
					x = int(192.5 - 128 * (0.5 + subX) / zoom)
361
					y = int(192.5 - 128 * (0.5 + subY) / zoom)
362
					if neighbour == 3:
363
						x -= 128 * weights[3] // factor
364
					elif neighbour == 5:
365
						x += 128 * weights[5] // factor
366
					else:
367
						assert neighbour is None, neighbour
368
					assert 0 <= x < 256, x
369
					assert 0 <= y < 256, y
370
					yield x
371
					yield y
372
373
# Main:
374
375
def process2x():
376
	pixelExpr = expandQuadrant(genExpr2(), 2)
377
378
	fullTableVariant = Variant(pixelExpr, lite = False, table = True)
379
	liteTableVariant = Variant(pixelExpr, lite = True,  table = True)
380
381
	#printText(formatOffsetsTable(fullTableVariant.pixelExpr))
382
	#printText(formatOffsetsTable(liteTableVariant.pixelExpr))
383
	#printText(formatWeightsTable(
384
	#	fullTableVariant.pixelExpr, computeWeightCells
385
	#	))
386
	#printText(formatWeightsTable(
387
	#	liteTableVariant.pixelExpr, computeLiteWeightCells
388
	#	))
389
390
	writeBinaryFile(
391
		'HQ2xOffsets.dat',
392
		computeOffsets(fullTableVariant.pixelExpr)
393
		)
394
	writeBinaryFile(
395
		'HQ2xWeights.dat',
396
		computeWeights(fullTableVariant.pixelExpr, computeWeightCells)
397
		)
398
	writeBinaryFile(
399
		'HQ2xLiteOffsets.dat',
400
		genHQLiteOffsetsTable(liteTableVariant.pixelExpr)
401
		)
402
	# Note: HQ2xLiteWeights.dat is not needed, since interpolated texture
403
	#       offsets can perform all the blending we need.
404
405
	Variant(pixelExpr, lite = False, table = False).writeSwitch(
406
		'HQ2xScaler-1x1to2x2.nn'
407
		)
408
	Variant(pixelExpr, lite = False, table = False, narrow = makeNarrow2to1).writeSwitch(
409
		'HQ2xScaler-1x1to1x2.nn'
410
		)
411
	Variant(pixelExpr, lite = True,  table = False).writeSwitch(
412
		'HQ2xLiteScaler-1x1to2x2.nn'
413
		)
414
	Variant(pixelExpr, lite = True,  table = False, narrow = makeNarrow2to1).writeSwitch(
415
		'HQ2xLiteScaler-1x1to1x2.nn'
416
		)
417
418
def process3x():
419
	pixelExpr = expandQuadrant(genExpr3(), 3)
420
421
	fullTableVariant = Variant(pixelExpr, lite = False, table = True)
422
	liteTableVariant = Variant(pixelExpr, lite = True,  table = True)
423
424
	#printText(formatOffsetsTable(fullTableVariant.pixelExpr))
425
	#printText(formatOffsetsTable(liteTableVariant.pixelExpr))
426
	#printText(formatWeightsTable(
427
		#fullTableVariant.pixelExpr, computeWeightCells
428
		#))
429
	#printText(formatWeightsTable(
430
		#liteTableVariant.pixelExpr, computeLiteWeightCells
431
		#))
432
433
	writeBinaryFile(
434
		'HQ3xOffsets.dat',
435
		computeOffsets(fullTableVariant.pixelExpr)
436
		)
437
	writeBinaryFile(
438
		'HQ3xWeights.dat',
439
		computeWeights(fullTableVariant.pixelExpr, computeWeightCells)
440
		)
441
	writeBinaryFile(
442
		'HQ3xLiteOffsets.dat',
443
		genHQLiteOffsetsTable(liteTableVariant.pixelExpr)
444
		)
445
	# Note: HQ3xLiteWeights.dat is not needed, since interpolated texture
446
	#       offsets can perform all the blending we need.
447
448
	Variant(pixelExpr, lite = False, table = False).writeSwitch(
449
		'HQ3xScaler-1x1to3x3.nn'
450
		)
451
	Variant(pixelExpr, lite = True,  table = False).writeSwitch(
452
		'HQ3xLiteScaler-1x1to3x3.nn'
453
		)
454
455
def process4x():
456
	pixelExpr = expandQuadrant(genExpr4(), 4)
457
458
	fullTableVariant = Variant(pixelExpr, lite = False, table = True)
459
	liteTableVariant = Variant(pixelExpr, lite = True,  table = True)
460
461
	#printText(formatOffsetsTable(fullTableVariant.pixelExpr))
462
	#printText(formatWeightsTable(fullTableVariant.pixelExpr, computeWeightCells))
463
	#printText(formatWeightsTable(liteTableVariant.pixelExpr, computeLiteWeightCells))
464
465
	writeBinaryFile(
466
		'HQ4xOffsets.dat',
467
		computeOffsets(fullTableVariant.pixelExpr)
468
		)
469
	writeBinaryFile(
470
		'HQ4xWeights.dat',
471
		computeWeights(fullTableVariant.pixelExpr, computeWeightCells)
472
		)
473
	writeBinaryFile(
474
		'HQ4xLiteOffsets.dat',
475
		genHQLiteOffsetsTable(liteTableVariant.pixelExpr)
476
		)
477
	# Note: HQ4xLiteWeights.dat is not needed, since interpolated texture
478
	#       offsets can perform all the blending we need.
479
480
	#printText(genSwitch(Variant(
481
		#pixelExpr, lite = False, table = False
482
		#).pixelExpr))
483
484
if __name__ == '__main__':
485
	process2x()
486
	process3x()
487
	process4x()