more perceptual optimisation

This commit is contained in:
minjaesong
2025-09-22 14:45:59 +09:00
parent 4851f61c56
commit e001445095
5 changed files with 442 additions and 248 deletions

View File

@@ -1,236 +0,0 @@
println("DEPRECATION NOTICE: MP3 Playback function will be removed for following reason")
println("\tMP3 does not really fit in the time TSVM targets to emulate")
return 1
const Mp3 = require('mp3dec')
const pcm = require("pcm")
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
function printdbg(s) { if (0) serial.println(s) }
class SequentialFileBuffer {
constructor(path, offset, length) {
if (Array.isArray(path)) throw Error("arg #1 is path(string), not array")
this.path = path
this.file = files.open(path)
this.offset = offset || 0
this.originalOffset = offset
this.length = length || this.file.size
this.seq = require("seqread")
this.seq.prepare(path)
}
/*readFull(n) {
throw Error()
let ptr = this.seq.readBytes(n)
return ptr
}*/
readStr(n) {
let ptr = this.seq.readBytes(n)
let s = ''
for (let i = 0; i < n; i++) {
if (i >= this.length) break
s += String.fromCharCode(sys.peek(ptr + i))
}
sys.free(ptr)
return s
}
readByteNumbers(n) {
let ptr = this.seq.readBytes(n)
try {
let s = []
for (let i = 0; i < n; i++) {
if (i >= this.length) break
s.push(sys.peek(ptr + i))
}
sys.free(ptr)
return s
}
catch (e) {
println(`n: ${n}; ptr: ${ptr}`)
println(e)
}
}
unread(diff) {
let newSkipLen = this.seq.getReadCount() - diff
this.seq.prepare(this.path)
this.seq.skip(newSkipLen)
}
rewind() {
this.seq.prepare(this.path)
}
seek(p) {
this.seq.prepare(this.path)
this.seq.skip(p)
}
get byteLength() {
return this.length
}
/*get remaining() {
return this.length - this.getReadCount()
}*/
}
con.curs_set(0)
let [cy, cx] = con.getyx()
let [__, CONSOLE_WIDTH] = con.getmaxyx()
let paintWidth = CONSOLE_WIDTH - 16
if (interactive) {
println("Decoding...")
}
printdbg("pre-decode...")
let filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
const FILE_SIZE = filebuf.length
let decoder = Mp3.newDecoder(filebuf)
if (decoder === null) throw Error("decoder is null")
const HEADER_SIZE = decoder.headerSize + 3
const FRAME_SIZE = decoder.frameSize // only works reliably for CBR
//serial.println(`header size: ${HEADER_SIZE}`)
//serial.println(`frame size: ${FRAME_SIZE}`)
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setPcmQueueCapacityIndex(0, 5) // queue size is now 24
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
audio.setMasterVolume(0, 255)
audio.play(0)
let decodedLength = 0
let readPtr = sys.malloc(8000)
let decodePtr = sys.malloc(12000)
function bytesToSec(i) {
return i / (FRAME_SIZE * 1000 / bufRealTimeLen)
}
function secToReadable(n) {
let mins = ''+((n/60)|0)
let secs = ''+(n % 60)
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
}
function decodeAndResample(inPtr, outPtr, inputLen) {
// TODO resample
for (let k = 0; k < inputLen / 2; k+=2) {
let sample = [
pcm.u16Tos16(sys.peek(inPtr + k*2 + 0) | (sys.peek(inPtr + k*2 + 1) << 8)),
pcm.u16Tos16(sys.peek(inPtr + k*2 + 2) | (sys.peek(inPtr + k*2 + 3) << 8))
]
sys.poke(outPtr + k, pcm.s16Tou8(sample[0]))
sys.poke(outPtr + k + 1, pcm.s16Tou8(sample[1]))
// soothing visualiser(????)
// printvis(`${sampleToVisual(sample[0])} | ${sampleToVisual(sample[1])}`)
}
}
function printPlayBar() {
}
let stopPlay = false
con.curs_set(0)
if (interactive) {
con.move(cy, cy)
println("Push and hold Backspace to exit")
}
[cy, cx] = con.getyx()
function printPlayBar(currently) {
if (interactive) {
// let currently = decodedLength
let total = FILE_SIZE - HEADER_SIZE
let currentlySec = Math.round(bytesToSec(currently))
let totalSec = Math.round(bytesToSec(total))
con.move(cy, 1)
print(' '.repeat(15))
con.move(cy, 1)
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
con.move(cy, 15)
print(' ')
let progressbar = '\x84205u'.repeat(paintWidth + 1)
print(progressbar)
con.mvaddch(cy, 16 + Math.round(paintWidth * (currently / total)), 0xDB)
}
}
let t1 = sys.nanoTime()
let errorlevel = 0
let bufRealTimeLen = 36
try {
decoder.decode((ptr, len, pos)=>{
if (interactive) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) {
stopPlay = true
throw "STOP"
}
}
printPlayBar(pos)
let t2 = sys.nanoTime()
decodedLength += len
// serial.println(`Audio queue size: ${audio.getPosition(0)}/${QUEUE_MAX}`)
if (audio.getPosition(0) >= QUEUE_MAX) {
while (audio.getPosition(0) >= (QUEUE_MAX >>> 1)) {
printdbg(`Queue full, waiting until the queue has some space (${audio.getPosition(0)}/${QUEUE_MAX})`)
// serial.println(`Queue full, waiting until the queue has some space (${audio.getPosition(0)}/${QUEUE_MAX})`)
sys.sleep(bufRealTimeLen)
}
}
decodeAndResample(ptr, decodePtr, len)
audio.putPcmDataByPtr(decodePtr, len >> 1, 0)
audio.setSampleUploadLength(0, len >> 1)
audio.startSampleUpload(0)
let decodingTime = (t2 - t1) / 1000000.0
bufRealTimeLen = (len >> 1) / 64000.0 * 1000
t1 = t2
printdbg(`Decoded ${decodedLength} bytes; target: ${bufRealTimeLen} ms, lag: ${decodingTime - bufRealTimeLen} ms`)
}) // now you got decoded PCM data
}
catch (e) {
if (e != "STOP") {
printerrln(e)
errorlevel = 1
}
}
finally {
//audio.stop(0)
sys.free(readPtr)
sys.free(decodePtr)
}
return errorlevel

View File

@@ -0,0 +1,357 @@
// TSVM Universal Cue Format (UCF) Player
// Created by Claude on 2025-09-22
// Usage: playucf cuefile.ucf [options]
// Options: -i (interactive mode)
if (!exec_args[1]) {
serial.println("Usage: playucf cuefile.ucf [options]")
serial.println("Options: -i (interactive mode)")
return 1
}
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
if (!files.exists(fullFilePath.full)) {
serial.println(`Error: File not found: ${fullFilePath.full}`)
return 2
}
// UCF Format constants
const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
const UCF_VERSION = 1
const ADDRESSING_EXTERNAL = 0x01
const ADDRESSING_INTERNAL = 0x02
// Media player mappings based on file extensions
const PLAYER_MAP = {
'mp2': 'playmp2',
'wav': 'playwav',
'pcm': 'playpcm',
'mv1': 'playmv1',
'mv2': 'playtev',
'mv3': 'playtav'
}
// Helper class for UCF file reading with internal addressing support
class UCFSequentialReader {
constructor(path, baseOffset = 0) {
this.path = path
this.baseOffset = baseOffset
this.currentOffset = 0
// Detect if this is a TAPE device path
if (path.startsWith("$:/TAPE") || path.startsWith("$:\\TAPE")) {
this.seq = require("seqreadtape")
} else {
this.seq = require("seqread")
}
this.seq.prepare(path)
// Skip to the base offset for internal addressing
if (baseOffset > 0) {
this.seq.skip(baseOffset)
this.currentOffset = baseOffset
}
}
readBytes(length) {
this.currentOffset += length
return this.seq.readBytes(length)
}
readOneByte() {
this.currentOffset += 1
return this.seq.readOneByte()
}
readShort() {
this.currentOffset += 2
return this.seq.readShort()
}
readString(length) {
this.currentOffset += length
return this.seq.readString(length)
}
skip(n) {
this.currentOffset += n
this.seq.skip(n)
}
// Skip to absolute position from base offset
seekTo(position) {
let targetOffset = this.baseOffset + position
if (targetOffset < this.currentOffset) {
// Need to rewind and seek forward
this.seq.prepare(this.path)
this.currentOffset = 0
if (targetOffset > 0) {
this.seq.skip(targetOffset)
this.currentOffset = targetOffset
}
} else if (targetOffset > this.currentOffset) {
// Skip forward
let skipAmount = targetOffset - this.currentOffset
this.seq.skip(skipAmount)
this.currentOffset = targetOffset
}
}
getPosition() {
return this.currentOffset - this.baseOffset
}
}
// Parse UCF file
serial.println(`Playing UCF: ${fullFilePath.full}`)
let reader = new UCFSequentialReader(fullFilePath.full)
// Read and validate magic
let magic = []
for (let i = 0; i < 8; i++) {
magic.push(reader.readOneByte())
}
let magicValid = true
for (let i = 0; i < 8; i++) {
if (magic[i] !== UCF_MAGIC[i]) {
magicValid = false
break
}
}
if (!magicValid) {
serial.println("Error: Invalid UCF magic signature")
return 3
}
// Read header
let version = reader.readOneByte()
if (version !== UCF_VERSION) {
serial.println(`Error: Unsupported UCF version: ${version} (expected ${UCF_VERSION})`)
return 4
}
let numElements = reader.readShort()
// Skip reserved bytes (5 bytes)
reader.skip(5)
serial.println(`UCF Version: ${version}, Elements: ${numElements}`)
// Parse cue elements
let cueElements = []
for (let i = 0; i < numElements; i++) {
let element = {}
element.addressingMode = reader.readOneByte()
let nameLength = reader.readShort()
element.name = reader.readString(nameLength)
if (element.addressingMode === ADDRESSING_EXTERNAL) {
let pathLength = reader.readShort()
element.path = reader.readString(pathLength)
serial.println(`Element ${i + 1}: ${element.name} -> ${element.path} (external)`)
} else if (element.addressingMode === ADDRESSING_INTERNAL) {
// Read 48-bit offset (6 bytes, little endian)
let offsetBytes = []
for (let j = 0; j < 6; j++) {
offsetBytes.push(reader.readOneByte())
}
element.offset = 0
for (let j = 0; j < 6; j++) {
element.offset |= (offsetBytes[j] << (j * 8))
}
serial.println(`Element ${i + 1}: ${element.name} -> offset ${element.offset} (internal)`)
} else {
serial.println(`Error: Unknown addressing mode: ${element.addressingMode}`)
return 5
}
cueElements.push(element)
}
// Function to get file extension
function getFileExtension(filename) {
let lastDot = filename.lastIndexOf('.')
if (lastDot === -1) return ''
return filename.substring(lastDot + 1).toLowerCase()
}
// Function to determine player for a file
function getPlayerForFile(filename) {
let ext = getFileExtension(filename)
return PLAYER_MAP[ext] || null
}
// Function to create a temporary file for internal addressing
function createTempFileForInternal(element, ucfPath) {
// Create a unique temporary filename
let tempFilename = `$:\\TMP\\temp_ucf_${Date.now()}_${element.name.replace(/[^a-zA-Z0-9]/g, '_')}`
// For internal addressing, we abuse seqread by creating a "virtual" file view
// We'll return a special path that our modified exec environment can handle
return {
isTemporary: true,
path: tempFilename,
ucfPath: ucfPath,
offset: element.offset,
name: element.name
}
}
// Play each cue element in sequence
for (let i = 0; i < cueElements.length; i++) {
let element = cueElements[i]
serial.println(`\nPlaying element ${i + 1}/${numElements}: ${element.name}`)
if (interactive && i > 0) {
serial.print("Press ENTER to continue, 'q' to quit: ")
let input = serial.readLine()
if (input && input.toLowerCase().startsWith('q')) {
serial.println("Playback stopped by user")
break
}
}
let playerFile = null
let targetPath = null
if (element.addressingMode === ADDRESSING_EXTERNAL) {
// External addressing - resolve relative path
let elementPath = element.path
if (!elementPath.startsWith('A:\\') && !elementPath.startsWith('A:/')) {
// Relative path - resolve relative to UCF file location
let ucfDir = fullFilePath.full.substring(0, fullFilePath.full.lastIndexOf('\\'))
targetPath = ucfDir + '\\' + elementPath.replace(/\//g, '\\')
} else {
targetPath = elementPath
}
if (!files.exists(targetPath)) {
serial.println(`Warning: External file not found: ${targetPath}`)
continue
}
playerFile = getPlayerForFile(element.name)
} else if (element.addressingMode === ADDRESSING_INTERNAL) {
// Internal addressing - create temporary file reference
let tempFile = createTempFileForInternal(element, fullFilePath.full)
targetPath = tempFile.path
playerFile = getPlayerForFile(element.name)
// For internal addressing, we need to extract the data to a temporary location
// or use a specialized player that can handle offset-based reading
// Since we can't easily create temp files, we'll modify the exec_args for the player
// Create a new UCF reader positioned at the file offset
let fileReader = new UCFSequentialReader(fullFilePath.full, element.offset)
// We need to somehow pass this to the player...
// The most elegant solution is to create a wrapper that temporarily modifies
// the file system view or uses a custom SequentialFileBuffer
// For now, let's use a simpler approach: save exec_args and restore them
let originalExecArgs = [...exec_args]
// Modify the global environment to provide the offset reader
let originalFilesOpen = files.open
files.open = function(path) {
if (path === targetPath || path.endsWith(targetPath)) {
// Return a mock file object that uses our offset reader
return {
exists: true,
size: 2147483648, // Arbitrary large size
path: path,
_ucfReader: fileReader
}
}
return originalFilesOpen.call(this, path)
}
// Also modify seqread require to use our reader
let originalRequire = require
require = function(moduleName) {
if (moduleName === "seqread" || moduleName === "seqreadtape") {
return {
prepare: function(path) {
if (path === targetPath || path.endsWith(targetPath)) {
// Already prepared in fileReader
return 0
}
return fileReader.seq.prepare(path)
},
readBytes: function(length, ptr) { return fileReader.readBytes(length, ptr) },
readOneByte: function() { return fileReader.readOneByte() },
readShort: function() { return fileReader.readShort() },
readInt: function() { return fileReader.seq.readInt() },
readFourCC: function() { return fileReader.seq.readFourCC() },
readString: function(length) { return fileReader.readString(length) },
skip: function(n) { return fileReader.skip(n) },
getReadCount: function() { return fileReader.getPosition() },
fileHeader: fileReader.seq.fileHeader
}
}
return originalRequire.call(this, moduleName)
}
try {
// Execute the player with modified environment
exec_args[1] = targetPath
if (playerFile) {
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
if (files.exists(playerPath)) {
eval(files.readText(playerPath))
} else {
serial.println(`Warning: Player not found: ${playerFile}`)
}
} else {
serial.println(`Warning: No player found for file type: ${element.name}`)
}
} catch (e) {
serial.println(`Error playing ${element.name}: ${e.message}`)
} finally {
// Restore original environment
files.open = originalFilesOpen
require = originalRequire
exec_args = originalExecArgs
}
continue
}
if (!playerFile) {
serial.println(`Warning: No player found for file type: ${element.name}`)
continue
}
// Execute the appropriate player
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
if (!files.exists(playerPath)) {
serial.println(`Warning: Player script not found: ${playerPath}`)
continue
}
// Save and modify exec_args for the player
let originalExecArgs = [...exec_args]
exec_args[1] = targetPath
try {
eval(files.readText(playerPath))
} catch (e) {
serial.println(`Error playing ${element.name}: ${e.message}`)
} finally {
// Restore original exec_args
exec_args = originalExecArgs
}
}
serial.println("\nUCF playback completed")
return 0

View File

@@ -954,6 +954,41 @@ Unlike the TEV format, TAV encoder emits extra sync packet for every 1000th fram
--------------------------------------------------------------------------------
TSVM Universal Cue format
Created by CuriousTorvald on 2025-09-22
A universal, simle cue designed to work as both playlist to cue up external files and lookup table for internal bytes.
# File Structure
\x1F T S V M U C F
[HEADER]
[CUE ELEMENT 0]
[CUE ELEMENT 1]
[CUE ELEMENT 2]
...
## Header (16 bytes)
uint8 Magic[8]: "\x1F TSVM UCF"
uint8 Version: 1
uint16 Number of cue elements
unit8 Reserved[5]
## Cue Element
uint8 Addressing Mode
- 0x01: External
- 0x02: Internal
uint16 String Length for name
* Name of the element in UTF-8
<if external addressing mode>
uint16 String Length for relative path
* Relative path
<if internal addressing mode>
uint48 Offset to the file
--------------------------------------------------------------------------------
Sound Adapter
Endianness: little
@@ -1092,6 +1127,44 @@ Play Head Flags
65536..131071 RW: PCM Sample buffer
Table of 3.5 Minifloat values (CSV)
,000,001,010,011,100,101,110,111,MSB
00000,0,1,2,4,8,16,32,64
00001,0.03125,1.03125,2.0625,4.125,8.25,16.5,33,66
00010,0.0625,1.0625,2.125,4.25,8.5,17,34,68
00011,0.09375,1.09375,2.1875,4.375,8.75,17.5,35,70
00100,0.125,1.125,2.25,4.5,9,18,36,72
00101,0.15625,1.15625,2.3125,4.625,9.25,18.5,37,74
00110,0.1875,1.1875,2.375,4.75,9.5,19,38,76
00111,0.21875,1.21875,2.4375,4.875,9.75,19.5,39,78
01000,0.25,1.25,2.5,5,10,20,40,80
01001,0.28125,1.28125,2.5625,5.125,10.25,20.5,41,82
01010,0.3125,1.3125,2.625,5.25,10.5,21,42,84
01011,0.34375,1.34375,2.6875,5.375,10.75,21.5,43,86
01100,0.375,1.375,2.75,5.5,11,22,44,88
01101,0.40625,1.40625,2.8125,5.625,11.25,22.5,45,90
01110,0.4375,1.4375,2.875,5.75,11.5,23,46,92
01111,0.46875,1.46875,2.9375,5.875,11.75,23.5,47,94
10000,0.5,1.5,3,6,12,24,48,96
10001,0.53125,1.53125,3.0625,6.125,12.25,24.5,49,98
10010,0.5625,1.5625,3.125,6.25,12.5,25,50,100
10011,0.59375,1.59375,3.1875,6.375,12.75,25.5,51,102
10100,0.625,1.625,3.25,6.5,13,26,52,104
10101,0.65625,1.65625,3.3125,6.625,13.25,26.5,53,106
10110,0.6875,1.6875,3.375,6.75,13.5,27,54,108
10111,0.71875,1.71875,3.4375,6.875,13.75,27.5,55,110
11000,0.75,1.75,3.5,7,14,28,56,112
11001,0.78125,1.78125,3.5625,7.125,14.25,28.5,57,114
11010,0.8125,1.8125,3.625,7.25,14.5,29,58,116
11011,0.84375,1.84375,3.6875,7.375,14.75,29.5,59,118
11100,0.875,1.875,3.75,7.5,15,30,60,120
11101,0.90625,1.90625,3.8125,7.625,15.25,30.5,61,122
11110,0.9375,1.9375,3.875,7.75,15.5,31,62,124
11111,0.96875,1.96875,3.9375,7.875,15.75,31.5,63,126
LSB
--------------------------------------------------------------------------------
RomBank / RamBank

View File

@@ -4091,8 +4091,8 @@ class GraphicsJSR223Delegate(private val vm: VM) {
var ANISOTROPY_MULT = floatArrayOf(1.8f, 1.6f, 1.4f, 1.2f, 1.0f, 1.0f)
var ANISOTROPY_BIAS = floatArrayOf(0.2f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f)
var ANISOTROPY_MULT_CHROMA = floatArrayOf(2.4f, 2.2f, 2.0f, 1.7f, 1.4f, 1.0f)
var ANISOTROPY_BIAS_CHROMA = floatArrayOf(0.4f, 0.3f, 0.2f, 0.1f, 0.0f, 0.0f)
var ANISOTROPY_MULT_CHROMA = floatArrayOf(6.6f, 5.5f, 4.4f, 3.3f, 2.2f, 1.1f)
var ANISOTROPY_BIAS_CHROMA = floatArrayOf(1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.0f)
@@ -4111,7 +4111,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
}
private fun perceptual_model3_HH(LH: Float, HL: Float): Float {
return 2f * (LH + HL) / 3f
return (HL / LH) * 1.44f;
}
fun perceptual_model3_LL(quality: Int, level: Int): Float {
@@ -4144,14 +4144,14 @@ class GraphicsJSR223Delegate(private val vm: VM) {
// LUMA CHANNEL: Based on statistical analysis from real video content
// LL subband - contains most image energy, preserve carefully
if (subbandType == 0) return perceptual_model3_LL(qualityLevel, level + 1)
if (subbandType == 0) return perceptual_model3_LL(qualityLevel, level)
// LH subband - horizontal details (human eyes more sensitive)
val LH: Float = perceptual_model3_LH(qualityLevel, level + 1)
val LH: Float = perceptual_model3_LH(qualityLevel, level)
if (subbandType == 1) return LH
// HL subband - vertical details
val HL: Float = perceptual_model3_HL(qualityLevel, LH + 1)
val HL: Float = perceptual_model3_HL(qualityLevel, LH)
if (subbandType == 2) return HL * (if (level == 2) TWO_PIXEL_DETAILER else if (level == 3) FOUR_PIXEL_DETAILER else 1f)
// HH subband - diagonal details

View File

@@ -152,8 +152,8 @@ static const int QUALITY_CG[] = {240, 180, 120, 60, 30, 5};
static const float ANISOTROPY_MULT[] = {1.8f, 1.6f, 1.4f, 1.2f, 1.0f, 1.0f};
static const float ANISOTROPY_BIAS[] = {0.2f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f};
static const float ANISOTROPY_MULT_CHROMA[] = {2.4f, 2.2f, 2.0f, 1.7f, 1.4f, 1.0f};
static const float ANISOTROPY_BIAS_CHROMA[] = {0.4f, 0.3f, 0.2f, 0.1f, 0.0f, 0.0f};
static const float ANISOTROPY_MULT_CHROMA[] = {6.6f, 5.5f, 4.4f, 3.3f, 2.2f, 1.1f};
static const float ANISOTROPY_BIAS_CHROMA[] = {1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.0f};
// DWT coefficient structure for each subband
typedef struct {
@@ -821,7 +821,7 @@ static float perceptual_model3_HL(int quality, float LH) {
}
static float perceptual_model3_HH(float LH, float HL) {
return 2.f * (LH + HL) / 3.f;
return (HL / LH) * 1.44f;
}
static float perceptual_model3_LL(int quality, int level) {
@@ -913,15 +913,15 @@ static float get_perceptual_weight(tav_encoder_t *enc, int level, int subband_ty
if (!is_chroma) {
// LL subband - contains most image energy, preserve carefully
if (subband_type == 0)
return perceptual_model3_LL(enc->quality_level, level + 1);
return perceptual_model3_LL(enc->quality_level, level);
// LH subband - horizontal details (human eyes more sensitive)
float LH = perceptual_model3_LH(enc->quality_level, level + 1);
float LH = perceptual_model3_LH(enc->quality_level, level);
if (subband_type == 1)
return LH;
// HL subband - vertical details
float HL = perceptual_model3_HL(enc->quality_level, LH + 1);
float HL = perceptual_model3_HL(enc->quality_level, LH);
if (subband_type == 2)
return HL * (level == 2 ? TWO_PIXEL_DETAILER : level == 3 ? FOUR_PIXEL_DETAILER : 1.0f);