|
1 GLSLGenerator = (function() { |
|
2 |
|
3 var vertexShaderTemplate = [ |
|
4 "attribute vec4 aPosition;", |
|
5 "", |
|
6 "varying vec4 vColor;", |
|
7 "", |
|
8 "$(extra)", |
|
9 "$(emu)", |
|
10 "", |
|
11 "void main()", |
|
12 "{", |
|
13 " gl_Position = aPosition;", |
|
14 " vec2 texcoord = vec2(aPosition.xy * 0.5 + vec2(0.5, 0.5));", |
|
15 " vec4 color = vec4(", |
|
16 " texcoord,", |
|
17 " texcoord.x * texcoord.y,", |
|
18 " (1.0 - texcoord.x) * texcoord.y * 0.5 + 0.5);", |
|
19 " $(test)", |
|
20 "}" |
|
21 ].join("\n"); |
|
22 |
|
23 var fragmentShaderTemplate = [ |
|
24 "#if defined(GL_ES)", |
|
25 "precision mediump float;", |
|
26 "#endif", |
|
27 "", |
|
28 "varying vec4 vColor;", |
|
29 "", |
|
30 "$(extra)", |
|
31 "$(emu)", |
|
32 "", |
|
33 "void main()", |
|
34 "{", |
|
35 " $(test)", |
|
36 "}" |
|
37 ].join("\n"); |
|
38 |
|
39 var baseVertexShader = [ |
|
40 "attribute vec4 aPosition;", |
|
41 "", |
|
42 "varying vec4 vColor;", |
|
43 "", |
|
44 "void main()", |
|
45 "{", |
|
46 " gl_Position = aPosition;", |
|
47 " vec2 texcoord = vec2(aPosition.xy * 0.5 + vec2(0.5, 0.5));", |
|
48 " vColor = vec4(", |
|
49 " texcoord,", |
|
50 " texcoord.x * texcoord.y,", |
|
51 " (1.0 - texcoord.x) * texcoord.y * 0.5 + 0.5);", |
|
52 "}" |
|
53 ].join("\n"); |
|
54 |
|
55 var baseFragmentShader = [ |
|
56 "#if defined(GL_ES)", |
|
57 "precision mediump float;", |
|
58 "#endif", |
|
59 "varying vec4 vColor;", |
|
60 "", |
|
61 "void main()", |
|
62 "{", |
|
63 " gl_FragColor = vColor;", |
|
64 "}" |
|
65 ].join("\n"); |
|
66 |
|
67 var types = [ |
|
68 { type: "float", |
|
69 code: [ |
|
70 "float $(func)_emu($(args)) {", |
|
71 " return $(func)_base($(baseArgs));", |
|
72 "}"].join("\n") |
|
73 }, |
|
74 { type: "vec2", |
|
75 code: [ |
|
76 "vec2 $(func)_emu($(args)) {", |
|
77 " return vec2(", |
|
78 " $(func)_base($(baseArgsX)),", |
|
79 " $(func)_base($(baseArgsY)));", |
|
80 "}"].join("\n") |
|
81 }, |
|
82 { type: "vec3", |
|
83 code: [ |
|
84 "vec3 $(func)_emu($(args)) {", |
|
85 " return vec3(", |
|
86 " $(func)_base($(baseArgsX)),", |
|
87 " $(func)_base($(baseArgsY)),", |
|
88 " $(func)_base($(baseArgsZ)));", |
|
89 "}"].join("\n") |
|
90 }, |
|
91 { type: "vec4", |
|
92 code: [ |
|
93 "vec4 $(func)_emu($(args)) {", |
|
94 " return vec4(", |
|
95 " $(func)_base($(baseArgsX)),", |
|
96 " $(func)_base($(baseArgsY)),", |
|
97 " $(func)_base($(baseArgsZ)),", |
|
98 " $(func)_base($(baseArgsW)));", |
|
99 "}"].join("\n") |
|
100 } |
|
101 ]; |
|
102 |
|
103 var bvecTypes = [ |
|
104 { type: "bvec2", |
|
105 code: [ |
|
106 "bvec2 $(func)_emu($(args)) {", |
|
107 " return bvec2(", |
|
108 " $(func)_base($(baseArgsX)),", |
|
109 " $(func)_base($(baseArgsY)));", |
|
110 "}"].join("\n") |
|
111 }, |
|
112 { type: "bvec3", |
|
113 code: [ |
|
114 "bvec3 $(func)_emu($(args)) {", |
|
115 " return bvec3(", |
|
116 " $(func)_base($(baseArgsX)),", |
|
117 " $(func)_base($(baseArgsY)),", |
|
118 " $(func)_base($(baseArgsZ)));", |
|
119 "}"].join("\n") |
|
120 }, |
|
121 { type: "bvec4", |
|
122 code: [ |
|
123 "vec4 $(func)_emu($(args)) {", |
|
124 " return bvec4(", |
|
125 " $(func)_base($(baseArgsX)),", |
|
126 " $(func)_base($(baseArgsY)),", |
|
127 " $(func)_base($(baseArgsZ)),", |
|
128 " $(func)_base($(baseArgsW)));", |
|
129 "}"].join("\n") |
|
130 } |
|
131 ]; |
|
132 |
|
133 var replaceRE = /\$\((\w+)\)/g; |
|
134 |
|
135 var replaceParams = function(str) { |
|
136 var args = arguments; |
|
137 return str.replace(replaceRE, function(str, p1, offset, s) { |
|
138 for (var ii = 1; ii < args.length; ++ii) { |
|
139 if (args[ii][p1] !== undefined) { |
|
140 return args[ii][p1]; |
|
141 } |
|
142 } |
|
143 throw "unknown string param '" + p1 + "'"; |
|
144 }); |
|
145 }; |
|
146 |
|
147 var generateReferenceShader = function( |
|
148 shaderInfo, template, params, typeInfo, test) { |
|
149 var input = shaderInfo.input; |
|
150 var output = shaderInfo.output; |
|
151 var feature = params.feature; |
|
152 var testFunc = params.testFunc; |
|
153 var emuFunc = params.emuFunc || ""; |
|
154 var extra = params.extra || ''; |
|
155 var args = params.args || "$(type) value"; |
|
156 var type = typeInfo.type; |
|
157 var typeCode = typeInfo.code; |
|
158 |
|
159 var baseArgs = params.baseArgs || "value$(field)"; |
|
160 var baseArgsX = replaceParams(baseArgs, {field: ".x"}); |
|
161 var baseArgsY = replaceParams(baseArgs, {field: ".y"}); |
|
162 var baseArgsZ = replaceParams(baseArgs, {field: ".z"}); |
|
163 var baseArgsW = replaceParams(baseArgs, {field: ".w"}); |
|
164 var baseArgs = replaceParams(baseArgs, {field: ""}); |
|
165 |
|
166 test = replaceParams(test, { |
|
167 input: input, |
|
168 output: output, |
|
169 func: feature + "_emu" |
|
170 }); |
|
171 emuFunc = replaceParams(emuFunc, { |
|
172 func: feature |
|
173 }); |
|
174 args = replaceParams(args, { |
|
175 type: type |
|
176 }); |
|
177 typeCode = replaceParams(typeCode, { |
|
178 func: feature, |
|
179 type: type, |
|
180 args: args, |
|
181 baseArgs: baseArgs, |
|
182 baseArgsX: baseArgsX, |
|
183 baseArgsY: baseArgsY, |
|
184 baseArgsZ: baseArgsZ, |
|
185 baseArgsW: baseArgsW |
|
186 }); |
|
187 var shader = replaceParams(template, { |
|
188 extra: extra, |
|
189 emu: emuFunc + "\n\n" + typeCode, |
|
190 test: test |
|
191 }); |
|
192 return shader; |
|
193 }; |
|
194 |
|
195 var generateTestShader = function( |
|
196 shaderInfo, template, params, test) { |
|
197 var input = shaderInfo.input; |
|
198 var output = shaderInfo.output; |
|
199 var feature = params.feature; |
|
200 var testFunc = params.testFunc; |
|
201 var extra = params.extra || ''; |
|
202 |
|
203 test = replaceParams(test, { |
|
204 input: input, |
|
205 output: output, |
|
206 func: feature |
|
207 }); |
|
208 var shader = replaceParams(template, { |
|
209 extra: extra, |
|
210 emu: '', |
|
211 test: test |
|
212 }); |
|
213 return shader; |
|
214 }; |
|
215 |
|
216 var runFeatureTest = function(params) { |
|
217 if (window.initNonKhronosFramework) { |
|
218 window.initNonKhronosFramework(false); |
|
219 } |
|
220 |
|
221 var wtu = WebGLTestUtils; |
|
222 var gridRes = params.gridRes; |
|
223 var vertexTolerance = params.tolerance || 0; |
|
224 var fragmentTolerance = vertexTolerance; |
|
225 if ('fragmentTolerance' in params) |
|
226 fragmentTolerance = params.fragmentTolerance || 0; |
|
227 |
|
228 description("Testing GLSL feature: " + params.feature); |
|
229 |
|
230 var width = 32; |
|
231 var height = 32; |
|
232 |
|
233 var console = document.getElementById("console"); |
|
234 var canvas = document.createElement('canvas'); |
|
235 canvas.width = width; |
|
236 canvas.height = height; |
|
237 var gl = wtu.create3DContext(canvas); |
|
238 if (!gl) { |
|
239 testFailed("context does not exist"); |
|
240 finishTest(); |
|
241 return; |
|
242 } |
|
243 |
|
244 var canvas2d = document.createElement('canvas'); |
|
245 canvas2d.width = width; |
|
246 canvas2d.height = height; |
|
247 var ctx = canvas2d.getContext("2d"); |
|
248 var imgData = ctx.getImageData(0, 0, width, height); |
|
249 |
|
250 var shaderInfos = [ |
|
251 { type: "vertex", |
|
252 input: "color", |
|
253 output: "vColor", |
|
254 vertexShaderTemplate: vertexShaderTemplate, |
|
255 fragmentShaderTemplate: baseFragmentShader, |
|
256 tolerance: vertexTolerance |
|
257 }, |
|
258 { type: "fragment", |
|
259 input: "vColor", |
|
260 output: "gl_FragColor", |
|
261 vertexShaderTemplate: baseVertexShader, |
|
262 fragmentShaderTemplate: fragmentShaderTemplate, |
|
263 tolerance: fragmentTolerance |
|
264 } |
|
265 ]; |
|
266 for (var ss = 0; ss < shaderInfos.length; ++ss) { |
|
267 var shaderInfo = shaderInfos[ss]; |
|
268 var tests = params.tests; |
|
269 var testTypes = params.emuFuncs || (params.bvecTest ? bvecTypes : types); |
|
270 // Test vertex shaders |
|
271 for (var ii = 0; ii < tests.length; ++ii) { |
|
272 var type = testTypes[ii]; |
|
273 if (params.simpleEmu) { |
|
274 type = { |
|
275 type: type.type, |
|
276 code: params.simpleEmu |
|
277 }; |
|
278 } |
|
279 debug(""); |
|
280 var str = replaceParams(params.testFunc, { |
|
281 func: params.feature, |
|
282 type: type.type, |
|
283 arg0: type.type |
|
284 }); |
|
285 debug("Testing: " + str + " in " + shaderInfo.type + " shader"); |
|
286 |
|
287 var referenceVertexShaderSource = generateReferenceShader( |
|
288 shaderInfo, |
|
289 shaderInfo.vertexShaderTemplate, |
|
290 params, |
|
291 type, |
|
292 tests[ii]); |
|
293 var referenceFragmentShaderSource = generateReferenceShader( |
|
294 shaderInfo, |
|
295 shaderInfo.fragmentShaderTemplate, |
|
296 params, |
|
297 type, |
|
298 tests[ii]); |
|
299 var testVertexShaderSource = generateTestShader( |
|
300 shaderInfo, |
|
301 shaderInfo.vertexShaderTemplate, |
|
302 params, |
|
303 tests[ii]); |
|
304 var testFragmentShaderSource = generateTestShader( |
|
305 shaderInfo, |
|
306 shaderInfo.fragmentShaderTemplate, |
|
307 params, |
|
308 tests[ii]); |
|
309 |
|
310 debug(""); |
|
311 wtu.addShaderSource( |
|
312 console, "reference vertex shader", referenceVertexShaderSource); |
|
313 wtu.addShaderSource( |
|
314 console, "reference fragment shader", referenceFragmentShaderSource); |
|
315 wtu.addShaderSource( |
|
316 console, "test vertex shader", testVertexShaderSource); |
|
317 wtu.addShaderSource( |
|
318 console, "test fragment shader", testFragmentShaderSource); |
|
319 debug(""); |
|
320 |
|
321 var refData = draw( |
|
322 canvas, referenceVertexShaderSource, referenceFragmentShaderSource); |
|
323 var refImg = wtu.makeImage(canvas); |
|
324 if (ss == 0) { |
|
325 var testData = draw( |
|
326 canvas, testVertexShaderSource, referenceFragmentShaderSource); |
|
327 } else { |
|
328 var testData = draw( |
|
329 canvas, referenceVertexShaderSource, testFragmentShaderSource); |
|
330 } |
|
331 var testImg = wtu.makeImage(canvas); |
|
332 |
|
333 reportResults(refData, refImg, testData, testImg, shaderInfo.tolerance); |
|
334 } |
|
335 } |
|
336 |
|
337 finishTest(); |
|
338 |
|
339 function reportResults(refData, refImage, testData, testImage, tolerance) { |
|
340 var same = true; |
|
341 for (var yy = 0; yy < height; ++yy) { |
|
342 for (var xx = 0; xx < width; ++xx) { |
|
343 var offset = (yy * width + xx) * 4; |
|
344 var imgOffset = ((height - yy - 1) * width + xx) * 4; |
|
345 imgData.data[imgOffset + 0] = 0; |
|
346 imgData.data[imgOffset + 1] = 0; |
|
347 imgData.data[imgOffset + 2] = 0; |
|
348 imgData.data[imgOffset + 3] = 255; |
|
349 if (Math.abs(refData[offset + 0] - testData[offset + 0]) > tolerance || |
|
350 Math.abs(refData[offset + 1] - testData[offset + 1]) > tolerance || |
|
351 Math.abs(refData[offset + 2] - testData[offset + 2]) > tolerance || |
|
352 Math.abs(refData[offset + 3] - testData[offset + 3]) > tolerance) { |
|
353 imgData.data[imgOffset] = 255; |
|
354 same = false; |
|
355 } |
|
356 } |
|
357 } |
|
358 |
|
359 var diffImg = null; |
|
360 if (!same) { |
|
361 ctx.putImageData(imgData, 0, 0); |
|
362 diffImg = wtu.makeImage(canvas2d); |
|
363 } |
|
364 |
|
365 var div = document.createElement("div"); |
|
366 div.className = "testimages"; |
|
367 wtu.insertImage(div, "ref", refImg); |
|
368 wtu.insertImage(div, "test", testImg); |
|
369 if (diffImg) { |
|
370 wtu.insertImage(div, "diff", diffImg); |
|
371 } |
|
372 div.appendChild(document.createElement('br')); |
|
373 |
|
374 |
|
375 console.appendChild(div); |
|
376 |
|
377 if (!same) { |
|
378 testFailed("images are different"); |
|
379 } else { |
|
380 testPassed("images are the same"); |
|
381 } |
|
382 |
|
383 console.appendChild(document.createElement('hr')); |
|
384 } |
|
385 |
|
386 function draw(canvas, vsSource, fsSource) { |
|
387 var program = wtu.loadProgram(gl, vsSource, fsSource, testFailed); |
|
388 |
|
389 var posLoc = gl.getAttribLocation(program, "aPosition"); |
|
390 WebGLTestUtils.setupQuad(gl, gridRes, posLoc); |
|
391 |
|
392 gl.useProgram(program); |
|
393 gl.clearColor(0, 0, 1, 1); |
|
394 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
|
395 gl.drawElements(gl.TRIANGLES, gridRes * gridRes * 6, gl.UNSIGNED_SHORT, 0); |
|
396 wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); |
|
397 |
|
398 var img = new Uint8Array(width * height * 4); |
|
399 gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); |
|
400 return img; |
|
401 } |
|
402 |
|
403 }; |
|
404 |
|
405 var runBasicTest = function(params) { |
|
406 if (window.initNonKhronosFramework) { |
|
407 window.initNonKhronosFramework(false); |
|
408 } |
|
409 |
|
410 var wtu = WebGLTestUtils; |
|
411 var gridRes = params.gridRes; |
|
412 var vertexTolerance = params.tolerance || 0; |
|
413 var fragmentTolerance = vertexTolerance; |
|
414 if ('fragmentTolerance' in params) |
|
415 fragmentTolerance = params.fragmentTolerance || 0; |
|
416 |
|
417 description("Testing : " + document.getElementsByTagName("title")[0].innerText); |
|
418 |
|
419 var width = 32; |
|
420 var height = 32; |
|
421 |
|
422 var console = document.getElementById("console"); |
|
423 var canvas = document.createElement('canvas'); |
|
424 canvas.width = width; |
|
425 canvas.height = height; |
|
426 var gl = wtu.create3DContext(canvas); |
|
427 if (!gl) { |
|
428 testFailed("context does not exist"); |
|
429 finishTest(); |
|
430 return; |
|
431 } |
|
432 |
|
433 var canvas2d = document.createElement('canvas'); |
|
434 canvas2d.width = width; |
|
435 canvas2d.height = height; |
|
436 var ctx = canvas2d.getContext("2d"); |
|
437 var imgData = ctx.getImageData(0, 0, width, height); |
|
438 |
|
439 var shaderInfos = [ |
|
440 { type: "vertex", |
|
441 input: "color", |
|
442 output: "vColor", |
|
443 vertexShaderTemplate: vertexShaderTemplate, |
|
444 fragmentShaderTemplate: baseFragmentShader, |
|
445 tolerance: vertexTolerance |
|
446 }, |
|
447 { type: "fragment", |
|
448 input: "vColor", |
|
449 output: "gl_FragColor", |
|
450 vertexShaderTemplate: baseVertexShader, |
|
451 fragmentShaderTemplate: fragmentShaderTemplate, |
|
452 tolerance: fragmentTolerance |
|
453 } |
|
454 ]; |
|
455 for (var ss = 0; ss < shaderInfos.length; ++ss) { |
|
456 var shaderInfo = shaderInfos[ss]; |
|
457 var tests = params.tests; |
|
458 // var testTypes = params.emuFuncs || (params.bvecTest ? bvecTypes : types); |
|
459 // Test vertex shaders |
|
460 for (var ii = 0; ii < tests.length; ++ii) { |
|
461 var test = tests[ii]; |
|
462 debug(""); |
|
463 debug("Testing: " + test.name + " in " + shaderInfo.type + " shader"); |
|
464 |
|
465 function genShader(shaderInfo, template, shader, subs) { |
|
466 shader = replaceParams(shader, subs, { |
|
467 input: shaderInfo.input, |
|
468 output: shaderInfo.output |
|
469 }); |
|
470 shader = replaceParams(template, subs, { |
|
471 test: shader, |
|
472 emu: "", |
|
473 extra: "" |
|
474 }); |
|
475 return shader; |
|
476 } |
|
477 |
|
478 var referenceVertexShaderSource = genShader( |
|
479 shaderInfo, |
|
480 shaderInfo.vertexShaderTemplate, |
|
481 test.reference.shader, |
|
482 test.reference.subs); |
|
483 var referenceFragmentShaderSource = genShader( |
|
484 shaderInfo, |
|
485 shaderInfo.fragmentShaderTemplate, |
|
486 test.reference.shader, |
|
487 test.reference.subs); |
|
488 var testVertexShaderSource = genShader( |
|
489 shaderInfo, |
|
490 shaderInfo.vertexShaderTemplate, |
|
491 test.test.shader, |
|
492 test.test.subs); |
|
493 var testFragmentShaderSource = genShader( |
|
494 shaderInfo, |
|
495 shaderInfo.fragmentShaderTemplate, |
|
496 test.test.shader, |
|
497 test.test.subs); |
|
498 |
|
499 debug(""); |
|
500 wtu.addShaderSource( |
|
501 console, "reference vertex shader", referenceVertexShaderSource); |
|
502 wtu.addShaderSource( |
|
503 console, "reference fragment shader", referenceFragmentShaderSource); |
|
504 wtu.addShaderSource( |
|
505 console, "test vertex shader", testVertexShaderSource); |
|
506 wtu.addShaderSource( |
|
507 console, "test fragment shader", testFragmentShaderSource); |
|
508 debug(""); |
|
509 |
|
510 var refData = draw( |
|
511 canvas, referenceVertexShaderSource, referenceFragmentShaderSource); |
|
512 var refImg = wtu.makeImage(canvas); |
|
513 if (ss == 0) { |
|
514 var testData = draw( |
|
515 canvas, testVertexShaderSource, referenceFragmentShaderSource); |
|
516 } else { |
|
517 var testData = draw( |
|
518 canvas, referenceVertexShaderSource, testFragmentShaderSource); |
|
519 } |
|
520 var testImg = wtu.makeImage(canvas); |
|
521 |
|
522 reportResults(refData, refImg, testData, testImg, shaderInfo.tolerance); |
|
523 } |
|
524 } |
|
525 |
|
526 finishTest(); |
|
527 |
|
528 function reportResults(refData, refImage, testData, testImage, tolerance) { |
|
529 var same = true; |
|
530 for (var yy = 0; yy < height; ++yy) { |
|
531 for (var xx = 0; xx < width; ++xx) { |
|
532 var offset = (yy * width + xx) * 4; |
|
533 var imgOffset = ((height - yy - 1) * width + xx) * 4; |
|
534 imgData.data[imgOffset + 0] = 0; |
|
535 imgData.data[imgOffset + 1] = 0; |
|
536 imgData.data[imgOffset + 2] = 0; |
|
537 imgData.data[imgOffset + 3] = 255; |
|
538 if (Math.abs(refData[offset + 0] - testData[offset + 0]) > tolerance || |
|
539 Math.abs(refData[offset + 1] - testData[offset + 1]) > tolerance || |
|
540 Math.abs(refData[offset + 2] - testData[offset + 2]) > tolerance || |
|
541 Math.abs(refData[offset + 3] - testData[offset + 3]) > tolerance) { |
|
542 imgData.data[imgOffset] = 255; |
|
543 same = false; |
|
544 } |
|
545 } |
|
546 } |
|
547 |
|
548 var diffImg = null; |
|
549 if (!same) { |
|
550 ctx.putImageData(imgData, 0, 0); |
|
551 diffImg = wtu.makeImage(canvas2d); |
|
552 } |
|
553 |
|
554 var div = document.createElement("div"); |
|
555 div.className = "testimages"; |
|
556 wtu.insertImage(div, "ref", refImg); |
|
557 wtu.insertImage(div, "test", testImg); |
|
558 if (diffImg) { |
|
559 wtu.insertImage(div, "diff", diffImg); |
|
560 } |
|
561 div.appendChild(document.createElement('br')); |
|
562 |
|
563 console.appendChild(div); |
|
564 |
|
565 if (!same) { |
|
566 testFailed("images are different"); |
|
567 } else { |
|
568 testPassed("images are the same"); |
|
569 } |
|
570 |
|
571 console.appendChild(document.createElement('hr')); |
|
572 } |
|
573 |
|
574 function draw(canvas, vsSource, fsSource) { |
|
575 var program = wtu.loadProgram(gl, vsSource, fsSource, testFailed); |
|
576 |
|
577 var posLoc = gl.getAttribLocation(program, "aPosition"); |
|
578 WebGLTestUtils.setupQuad(gl, gridRes, posLoc); |
|
579 |
|
580 gl.useProgram(program); |
|
581 gl.clearColor(0, 0, 1, 1); |
|
582 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
|
583 gl.drawElements(gl.TRIANGLES, gridRes * gridRes * 6, gl.UNSIGNED_SHORT, 0); |
|
584 wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); |
|
585 |
|
586 var img = new Uint8Array(width * height * 4); |
|
587 gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); |
|
588 return img; |
|
589 } |
|
590 |
|
591 }; |
|
592 |
|
593 var runReferenceImageTest = function(params) { |
|
594 if (window.initNonKhronosFramework) { |
|
595 window.initNonKhronosFramework(false); |
|
596 } |
|
597 |
|
598 var wtu = WebGLTestUtils; |
|
599 var gridRes = params.gridRes; |
|
600 var vertexTolerance = params.tolerance || 0; |
|
601 var fragmentTolerance = vertexTolerance; |
|
602 if ('fragmentTolerance' in params) |
|
603 fragmentTolerance = params.fragmentTolerance || 0; |
|
604 |
|
605 description("Testing GLSL feature: " + params.feature); |
|
606 |
|
607 var width = 32; |
|
608 var height = 32; |
|
609 |
|
610 var console = document.getElementById("console"); |
|
611 var canvas = document.createElement('canvas'); |
|
612 canvas.width = width; |
|
613 canvas.height = height; |
|
614 var gl = wtu.create3DContext(canvas, { antialias: false }); |
|
615 if (!gl) { |
|
616 testFailed("context does not exist"); |
|
617 finishTest(); |
|
618 return; |
|
619 } |
|
620 |
|
621 var canvas2d = document.createElement('canvas'); |
|
622 canvas2d.width = width; |
|
623 canvas2d.height = height; |
|
624 var ctx = canvas2d.getContext("2d"); |
|
625 var imgData = ctx.getImageData(0, 0, width, height); |
|
626 |
|
627 var shaderInfos = [ |
|
628 { type: "vertex", |
|
629 input: "color", |
|
630 output: "vColor", |
|
631 vertexShaderTemplate: vertexShaderTemplate, |
|
632 fragmentShaderTemplate: baseFragmentShader, |
|
633 tolerance: vertexTolerance |
|
634 }, |
|
635 { type: "fragment", |
|
636 input: "vColor", |
|
637 output: "gl_FragColor", |
|
638 vertexShaderTemplate: baseVertexShader, |
|
639 fragmentShaderTemplate: fragmentShaderTemplate, |
|
640 tolerance: fragmentTolerance |
|
641 } |
|
642 ]; |
|
643 for (var ss = 0; ss < shaderInfos.length; ++ss) { |
|
644 var shaderInfo = shaderInfos[ss]; |
|
645 var tests = params.tests; |
|
646 var testTypes = params.emuFuncs || (params.bvecTest ? bvecTypes : types); |
|
647 // Test vertex shaders |
|
648 for (var ii = 0; ii < tests.length; ++ii) { |
|
649 var type = testTypes[ii]; |
|
650 var isVertex = (ss == 0); |
|
651 debug(""); |
|
652 var str = replaceParams(params.testFunc, { |
|
653 func: params.feature, |
|
654 type: type.type, |
|
655 arg0: type.type |
|
656 }); |
|
657 debug("Testing: " + str + " in " + shaderInfo.type + " shader"); |
|
658 |
|
659 var referenceVertexShaderSource = generateReferenceShader( |
|
660 shaderInfo, |
|
661 shaderInfo.vertexShaderTemplate, |
|
662 params, |
|
663 type, |
|
664 tests[ii].source); |
|
665 var referenceFragmentShaderSource = generateReferenceShader( |
|
666 shaderInfo, |
|
667 shaderInfo.fragmentShaderTemplate, |
|
668 params, |
|
669 type, |
|
670 tests[ii].source); |
|
671 var testVertexShaderSource = generateTestShader( |
|
672 shaderInfo, |
|
673 shaderInfo.vertexShaderTemplate, |
|
674 params, |
|
675 tests[ii].source); |
|
676 var testFragmentShaderSource = generateTestShader( |
|
677 shaderInfo, |
|
678 shaderInfo.fragmentShaderTemplate, |
|
679 params, |
|
680 tests[ii].source); |
|
681 var referenceTexture = generateReferenceTexture( |
|
682 gl, |
|
683 tests[ii].generator, |
|
684 isVertex ? gridRes : width, |
|
685 isVertex ? gridRes : height, |
|
686 isVertex); |
|
687 |
|
688 debug(""); |
|
689 wtu.addShaderSource( |
|
690 console, "test vertex shader", testVertexShaderSource); |
|
691 wtu.addShaderSource( |
|
692 console, "test fragment shader", testFragmentShaderSource); |
|
693 debug(""); |
|
694 var refData = drawReferenceImage(canvas, referenceTexture, isVertex); |
|
695 var refImg = wtu.makeImage(canvas); |
|
696 if (isVertex) { |
|
697 var testData = draw( |
|
698 canvas, testVertexShaderSource, referenceFragmentShaderSource); |
|
699 } else { |
|
700 var testData = draw( |
|
701 canvas, referenceVertexShaderSource, testFragmentShaderSource); |
|
702 } |
|
703 var testImg = wtu.makeImage(canvas); |
|
704 var testTolerance = shaderInfo.tolerance; |
|
705 // Provide per-test tolerance so that we can increase it only for those desired. |
|
706 if ('tolerance' in tests[ii]) |
|
707 testTolerance = tests[ii].tolerance || 0; |
|
708 reportResults(refData, refImg, testData, testImg, testTolerance); |
|
709 } |
|
710 } |
|
711 |
|
712 finishTest(); |
|
713 |
|
714 function reportResults(refData, refImage, testData, testImage, tolerance) { |
|
715 var same = true; |
|
716 for (var yy = 0; yy < height; ++yy) { |
|
717 for (var xx = 0; xx < width; ++xx) { |
|
718 var offset = (yy * width + xx) * 4; |
|
719 var imgOffset = ((height - yy - 1) * width + xx) * 4; |
|
720 imgData.data[imgOffset + 0] = 0; |
|
721 imgData.data[imgOffset + 1] = 0; |
|
722 imgData.data[imgOffset + 2] = 0; |
|
723 imgData.data[imgOffset + 3] = 255; |
|
724 if (Math.abs(refData[offset + 0] - testData[offset + 0]) > tolerance || |
|
725 Math.abs(refData[offset + 1] - testData[offset + 1]) > tolerance || |
|
726 Math.abs(refData[offset + 2] - testData[offset + 2]) > tolerance || |
|
727 Math.abs(refData[offset + 3] - testData[offset + 3]) > tolerance) { |
|
728 console.appendChild(document.createTextNode('at (' + xx + ',' + yy + '): ref=(' + |
|
729 refData[offset + 0] + ',' + |
|
730 refData[offset + 1] + ',' + |
|
731 refData[offset + 2] + ',' + |
|
732 refData[offset + 3] + ') test=(' + |
|
733 testData[offset + 0] + ',' + |
|
734 testData[offset + 1] + ',' + |
|
735 testData[offset + 2] + ',' + |
|
736 testData[offset + 3] + ')')); |
|
737 console.appendChild(document.createElement('br')); |
|
738 |
|
739 |
|
740 |
|
741 imgData.data[imgOffset] = 255; |
|
742 same = false; |
|
743 } |
|
744 } |
|
745 } |
|
746 |
|
747 var diffImg = null; |
|
748 if (!same) { |
|
749 ctx.putImageData(imgData, 0, 0); |
|
750 diffImg = wtu.makeImage(canvas2d); |
|
751 } |
|
752 |
|
753 var div = document.createElement("div"); |
|
754 div.className = "testimages"; |
|
755 wtu.insertImage(div, "ref", refImg); |
|
756 wtu.insertImage(div, "test", testImg); |
|
757 if (diffImg) { |
|
758 wtu.insertImage(div, "diff", diffImg); |
|
759 } |
|
760 div.appendChild(document.createElement('br')); |
|
761 |
|
762 console.appendChild(div); |
|
763 |
|
764 if (!same) { |
|
765 testFailed("images are different"); |
|
766 } else { |
|
767 testPassed("images are the same"); |
|
768 } |
|
769 |
|
770 console.appendChild(document.createElement('hr')); |
|
771 } |
|
772 |
|
773 function draw(canvas, vsSource, fsSource) { |
|
774 var program = wtu.loadProgram(gl, vsSource, fsSource, testFailed); |
|
775 |
|
776 var posLoc = gl.getAttribLocation(program, "aPosition"); |
|
777 WebGLTestUtils.setupQuad(gl, gridRes, posLoc); |
|
778 |
|
779 gl.useProgram(program); |
|
780 gl.clearColor(0, 0, 1, 1); |
|
781 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
|
782 gl.drawElements(gl.TRIANGLES, gridRes * gridRes * 6, gl.UNSIGNED_SHORT, 0); |
|
783 wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); |
|
784 |
|
785 var img = new Uint8Array(width * height * 4); |
|
786 gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); |
|
787 return img; |
|
788 } |
|
789 |
|
790 function drawReferenceImage(canvas, texture, isVertex) { |
|
791 var program; |
|
792 if (isVertex) { |
|
793 var halfTexel = 0.5 / (1.0 + gridRes); |
|
794 program = WebGLTestUtils.setupTexturedQuadWithTexCoords( |
|
795 gl, [halfTexel, halfTexel], [1.0 - halfTexel, 1.0 - halfTexel]); |
|
796 } else { |
|
797 program = WebGLTestUtils.setupTexturedQuad(gl); |
|
798 } |
|
799 |
|
800 gl.activeTexture(gl.TEXTURE0); |
|
801 gl.bindTexture(gl.TEXTURE_2D, texture); |
|
802 var texLoc = gl.getUniformLocation(program, "tex"); |
|
803 gl.uniform1i(texLoc, 0); |
|
804 wtu.drawQuad(gl); |
|
805 wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors from draw"); |
|
806 |
|
807 var img = new Uint8Array(width * height * 4); |
|
808 gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img); |
|
809 return img; |
|
810 } |
|
811 |
|
812 /** |
|
813 * Creates and returns a texture containing the reference image for |
|
814 * the function being tested. Exactly how the function is evaluated, |
|
815 * and the size of the returned texture, depends on whether we are |
|
816 * testing a vertex or fragment shader. If a fragment shader, the |
|
817 * function is evaluated at the pixel centers. If a vertex shader, |
|
818 * the function is evaluated at the triangle's vertices, and the |
|
819 * resulting texture must be offset by half a texel during |
|
820 * rendering. |
|
821 * |
|
822 * @param {!WebGLRenderingContext} gl The WebGLRenderingContext to use to generate texture objects. |
|
823 * @param {!function(number,number,number,number): !Array.<number>} generator The reference image generator function. |
|
824 * @param {number} width The width of the texture to generate if testing a fragment shader; the grid resolution if testing a vertex shader. |
|
825 * @param {number} height The height of the texture to generate if testing a fragment shader; the grid resolution if testing a vertex shader. |
|
826 * @param {boolean} isVertex True if generating a reference image for a vertex shader; false if for a fragment shader. |
|
827 * @return {!WebGLTexture} The texture object that was generated. |
|
828 */ |
|
829 function generateReferenceTexture( |
|
830 gl, |
|
831 generator, |
|
832 width, |
|
833 height, |
|
834 isVertex) { |
|
835 |
|
836 // Note: the math in this function must match that in the vertex and |
|
837 // fragment shader templates above. |
|
838 function computeTexCoord(x) { |
|
839 return x * 0.5 + 0.5; |
|
840 } |
|
841 |
|
842 function computeColor(texCoordX, texCoordY) { |
|
843 return [ texCoordX, |
|
844 texCoordY, |
|
845 texCoordX * texCoordY, |
|
846 (1.0 - texCoordX) * texCoordY * 0.5 + 0.5 ]; |
|
847 } |
|
848 |
|
849 function clamp(value, minVal, maxVal) { |
|
850 return Math.max(minVal, Math.min(value, maxVal)); |
|
851 } |
|
852 |
|
853 // Evaluates the function at clip coordinates (px,py), storing the |
|
854 // result in the array "pixel". Each channel's result is clamped |
|
855 // between 0 and 255. |
|
856 function evaluateAtClipCoords(px, py, pixel) { |
|
857 var tcx = computeTexCoord(px); |
|
858 var tcy = computeTexCoord(py); |
|
859 |
|
860 var color = computeColor(tcx, tcy); |
|
861 |
|
862 var output = generator(color[0], color[1], color[2], color[3]); |
|
863 |
|
864 // Multiply by 256 to get even distribution for all values between 0 and 1. |
|
865 // Use rounding rather than truncation to more closely match the GPU's behavior. |
|
866 pixel[0] = clamp(Math.round(256 * output[0]), 0, 255); |
|
867 pixel[1] = clamp(Math.round(256 * output[1]), 0, 255); |
|
868 pixel[2] = clamp(Math.round(256 * output[2]), 0, 255); |
|
869 pixel[3] = clamp(Math.round(256 * output[3]), 0, 255); |
|
870 } |
|
871 |
|
872 function fillFragmentReference() { |
|
873 var data = new Uint8Array(4 * width * height); |
|
874 |
|
875 var horizTexel = 1.0 / width; |
|
876 var vertTexel = 1.0 / height; |
|
877 var halfHorizTexel = 0.5 * horizTexel; |
|
878 var halfVertTexel = 0.5 * vertTexel; |
|
879 |
|
880 var pixel = new Array(4); |
|
881 |
|
882 for (var yi = 0; yi < height; ++yi) { |
|
883 for (var xi = 0; xi < width; ++xi) { |
|
884 // The function must be evaluated at pixel centers. |
|
885 |
|
886 // Compute desired position in clip space |
|
887 var px = -1.0 + 2.0 * (halfHorizTexel + xi * horizTexel); |
|
888 var py = -1.0 + 2.0 * (halfVertTexel + yi * vertTexel); |
|
889 |
|
890 evaluateAtClipCoords(px, py, pixel); |
|
891 var index = 4 * (width * yi + xi); |
|
892 data[index + 0] = pixel[0]; |
|
893 data[index + 1] = pixel[1]; |
|
894 data[index + 2] = pixel[2]; |
|
895 data[index + 3] = pixel[3]; |
|
896 } |
|
897 } |
|
898 |
|
899 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, |
|
900 gl.RGBA, gl.UNSIGNED_BYTE, data); |
|
901 } |
|
902 |
|
903 function fillVertexReference() { |
|
904 // We generate a texture which contains the evaluation of the |
|
905 // function at the vertices of the triangle mesh. It is expected |
|
906 // that the width and the height are identical, and equivalent |
|
907 // to the grid resolution. |
|
908 if (width != height) { |
|
909 throw "width and height must be equal"; |
|
910 } |
|
911 |
|
912 var texSize = 1 + width; |
|
913 var data = new Uint8Array(4 * texSize * texSize); |
|
914 |
|
915 var step = 2.0 / width; |
|
916 |
|
917 var pixel = new Array(4); |
|
918 |
|
919 for (var yi = 0; yi < texSize; ++yi) { |
|
920 for (var xi = 0; xi < texSize; ++xi) { |
|
921 // The function is evaluated at the triangles' vertices. |
|
922 |
|
923 // Compute desired position in clip space |
|
924 var px = -1.0 + (xi * step); |
|
925 var py = -1.0 + (yi * step); |
|
926 |
|
927 evaluateAtClipCoords(px, py, pixel); |
|
928 var index = 4 * (texSize * yi + xi); |
|
929 data[index + 0] = pixel[0]; |
|
930 data[index + 1] = pixel[1]; |
|
931 data[index + 2] = pixel[2]; |
|
932 data[index + 3] = pixel[3]; |
|
933 } |
|
934 } |
|
935 |
|
936 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texSize, texSize, 0, |
|
937 gl.RGBA, gl.UNSIGNED_BYTE, data); |
|
938 } |
|
939 |
|
940 //---------------------------------------------------------------------- |
|
941 // Body of generateReferenceTexture |
|
942 // |
|
943 |
|
944 var texture = gl.createTexture(); |
|
945 gl.bindTexture(gl.TEXTURE_2D, texture); |
|
946 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
|
947 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); |
|
948 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
|
949 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
|
950 |
|
951 if (isVertex) { |
|
952 fillVertexReference(); |
|
953 } else { |
|
954 fillFragmentReference(); |
|
955 } |
|
956 |
|
957 return texture; |
|
958 } |
|
959 }; |
|
960 |
|
961 return { |
|
962 /** |
|
963 * runs a bunch of GLSL tests using the passed in parameters |
|
964 * The parameters are: |
|
965 * |
|
966 * feature: |
|
967 * the name of the function being tested (eg, sin, dot, |
|
968 * normalize) |
|
969 * |
|
970 * testFunc: |
|
971 * The prototype of function to be tested not including the |
|
972 * return type. |
|
973 * |
|
974 * emuFunc: |
|
975 * A base function that can be used to generate emulation |
|
976 * functions. Example for 'ceil' |
|
977 * |
|
978 * float $(func)_base(float value) { |
|
979 * float m = mod(value, 1.0); |
|
980 * return m != 0.0 ? (value + 1.0 - m) : value; |
|
981 * } |
|
982 * |
|
983 * args: |
|
984 * The arguments to the function |
|
985 * |
|
986 * baseArgs: (optional) |
|
987 * The arguments when a base function is used to create an |
|
988 * emulation function. For example 'float sign_base(float v)' |
|
989 * is used to implemenent vec2 sign_emu(vec2 v). |
|
990 * |
|
991 * simpleEmu: |
|
992 * if supplied, the code that can be used to generate all |
|
993 * functions for all types. |
|
994 * |
|
995 * Example for 'normalize': |
|
996 * |
|
997 * $(type) $(func)_emu($(args)) { |
|
998 * return value / length(value); |
|
999 * } |
|
1000 * |
|
1001 * gridRes: (optional) |
|
1002 * The resolution of the mesh to generate. The default is a |
|
1003 * 1x1 grid but many vertex shaders need a higher resolution |
|
1004 * otherwise the only values passed in are the 4 corners |
|
1005 * which often have the same value. |
|
1006 * |
|
1007 * tests: |
|
1008 * The code for each test. It is assumed the tests are for |
|
1009 * float, vec2, vec3, vec4 in that order. |
|
1010 * |
|
1011 * tolerance: (optional) |
|
1012 * Allow some tolerance in the comparisons. The tolerance is applied to |
|
1013 * both vertex and fragment shaders. The default tolerance is 0, meaning |
|
1014 * the values have to be identical. |
|
1015 * |
|
1016 * fragmentTolerance: (optional) |
|
1017 * Specify a tolerance which only applies to fragment shaders. The |
|
1018 * fragment-only tolerance will override the shared tolerance for |
|
1019 * fragment shaders if both are specified. Fragment shaders usually |
|
1020 * use mediump float precision so they sometimes require higher tolerance |
|
1021 * than vertex shaders which use highp by default. |
|
1022 */ |
|
1023 runFeatureTest: runFeatureTest, |
|
1024 |
|
1025 /* |
|
1026 * Runs a bunch of GLSL tests using the passed in parameters |
|
1027 * |
|
1028 * The parameters are: |
|
1029 * |
|
1030 * tests: |
|
1031 * Array of tests. For each test the following parameters are expected |
|
1032 * |
|
1033 * name: |
|
1034 * some description of the test |
|
1035 * reference: |
|
1036 * parameters for the reference shader (see below) |
|
1037 * test: |
|
1038 * parameters for the test shader (see below) |
|
1039 * |
|
1040 * The parameter for the reference and test shaders are |
|
1041 * |
|
1042 * shader: the GLSL for the shader |
|
1043 * subs: any substitutions you wish to define for the shader. |
|
1044 * |
|
1045 * Each shader is created from a basic template that |
|
1046 * defines an input and an output. You can see the |
|
1047 * templates at the top of this file. The input and output |
|
1048 * change depending on whether or not we are generating |
|
1049 * a vertex or fragment shader. |
|
1050 * |
|
1051 * All this code function does is a bunch of string substitutions. |
|
1052 * A substitution is defined by $(name). If name is found in |
|
1053 * the 'subs' parameter it is replaced. 4 special names exist. |
|
1054 * |
|
1055 * 'input' the input to your GLSL. Always a vec4. All change |
|
1056 * from 0 to 1 over the quad to be drawn. |
|
1057 * |
|
1058 * 'output' the output color. Also a vec4 |
|
1059 * |
|
1060 * 'emu' a place to insert extra stuff |
|
1061 * 'extra' a place to insert extra stuff. |
|
1062 * |
|
1063 * You can think of the templates like this |
|
1064 * |
|
1065 * $(extra) |
|
1066 * $(emu) |
|
1067 * |
|
1068 * void main() { |
|
1069 * // do math to calculate input |
|
1070 * ... |
|
1071 * |
|
1072 * $(shader) |
|
1073 * } |
|
1074 * |
|
1075 * Your shader first has any subs you provided applied as well |
|
1076 * as 'input' and 'output' |
|
1077 * |
|
1078 * It is then inserted into the template which is also provided |
|
1079 * with your subs. |
|
1080 * |
|
1081 * gridRes: (optional) |
|
1082 * The resolution of the mesh to generate. The default is a |
|
1083 * 1x1 grid but many vertex shaders need a higher resolution |
|
1084 * otherwise the only values passed in are the 4 corners |
|
1085 * which often have the same value. |
|
1086 * |
|
1087 * tolerance: (optional) |
|
1088 * Allow some tolerance in the comparisons. The tolerance is applied to |
|
1089 * both vertex and fragment shaders. The default tolerance is 0, meaning |
|
1090 * the values have to be identical. |
|
1091 * |
|
1092 * fragmentTolerance: (optional) |
|
1093 * Specify a tolerance which only applies to fragment shaders. The |
|
1094 * fragment-only tolerance will override the shared tolerance for |
|
1095 * fragment shaders if both are specified. Fragment shaders usually |
|
1096 * use mediump float precision so they sometimes require higher tolerance |
|
1097 * than vertex shaders which use highp. |
|
1098 */ |
|
1099 runBasicTest: runBasicTest, |
|
1100 |
|
1101 /** |
|
1102 * Runs a bunch of GLSL tests using the passed in parameters. The |
|
1103 * expected results are computed as a reference image in JavaScript |
|
1104 * instead of on the GPU. The parameters are: |
|
1105 * |
|
1106 * feature: |
|
1107 * the name of the function being tested (eg, sin, dot, |
|
1108 * normalize) |
|
1109 * |
|
1110 * testFunc: |
|
1111 * The prototype of function to be tested not including the |
|
1112 * return type. |
|
1113 * |
|
1114 * args: |
|
1115 * The arguments to the function |
|
1116 * |
|
1117 * gridRes: (optional) |
|
1118 * The resolution of the mesh to generate. The default is a |
|
1119 * 1x1 grid but many vertex shaders need a higher resolution |
|
1120 * otherwise the only values passed in are the 4 corners |
|
1121 * which often have the same value. |
|
1122 * |
|
1123 * tests: |
|
1124 * Array of tests. It is assumed the tests are for float, vec2, |
|
1125 * vec3, vec4 in that order. For each test the following |
|
1126 * parameters are expected: |
|
1127 * |
|
1128 * source: the GLSL source code for the tests |
|
1129 * |
|
1130 * generator: a JavaScript function taking four parameters |
|
1131 * which evaluates the same function as the GLSL source, |
|
1132 * returning its result as a newly allocated array. |
|
1133 * |
|
1134 * tolerance: (optional) a per-test tolerance. |
|
1135 * |
|
1136 * extra: (optional) |
|
1137 * Extra GLSL code inserted at the top of each test's shader. |
|
1138 * |
|
1139 * tolerance: (optional) |
|
1140 * Allow some tolerance in the comparisons. The tolerance is applied to |
|
1141 * both vertex and fragment shaders. The default tolerance is 0, meaning |
|
1142 * the values have to be identical. |
|
1143 * |
|
1144 * fragmentTolerance: (optional) |
|
1145 * Specify a tolerance which only applies to fragment shaders. The |
|
1146 * fragment-only tolerance will override the shared tolerance for |
|
1147 * fragment shaders if both are specified. Fragment shaders usually |
|
1148 * use mediump float precision so they sometimes require higher tolerance |
|
1149 * than vertex shaders which use highp. |
|
1150 */ |
|
1151 runReferenceImageTest: runReferenceImageTest, |
|
1152 |
|
1153 none: false |
|
1154 }; |
|
1155 |
|
1156 }()); |
|
1157 |