Saturday, December 1, 2018

San Andreas "Modern" Modding Tools & Some Rant

When I search for some tutorials in Google about modding GTA San Andreas, it's mostly still uses very old tool. Usually it's Indonesian modding community that always uses old modding tools for "compatibility" reason, but in fact old tools were unstable, and of course newer one means more stable (although may need modern system). For todays blog post, I just want to tell you that there are better tools out there that you can use as replacement. Note that this blog post can be considered as rant, depending on your perspective.

First, IMG editor. This is essential tool for modifying the game models and textures. Usually, the blog post tells you to use "IMG Tool v2.0". Excuse me, there's a way better tool for this, and it's Alci's IMG Editor. Compared to IMG Tool v2.0, Alci's IMG Editor is way more superior. Having bigger windows, import/export multiple files, and most importantly, it can create new IMG file from scratch, unlike IMG Tool, where you only restricted to existing IMG file.

Second, TXD tool. This is must-have tool if you plan to modify texture files (creating your own paintjob). Usually, the blog post tells you to use "TXD Workshop", but excuse me, TXD Workshop is somewhat very limited, and last time I tired the 15 years edition, the mipmap generation is buggy. If you interested how it buggy, my image is 4096x4096, compressed to DXT1, then generate 12 levels of mipmaps. It took around 3 minutes in my i5 7th gen laptop, and the result, it fails to generate mipmap for odd-levels (3, 5, 7, ...). This result in "black shade" when the game render the texture. So let me introduce you to Magic.TXD,  modern version of TXD Workshop, easy to use, and it generates mipmap faster and not buggy of course. It also comes with localization popular among the SA modders including Indonesian.

The texture created with TXD Workshop + mipmap. Level 3, 5, 7, and 11 is buggy and game will render that to black (notice shade of black near the headlights). That's why it looks dark for some reason.
The texture create with Magic.TXD. Now everything looks nice and no more black shade.

For this one, it's not really modding tool, but I want to put it anyway. If you're script modder, then you may want to consider MoonLoader, which is "modern version" of CLEO. Do you know that CLEO scrips are actually slow compared to Lua script? or, you hate CLEO scripts and prefer writing in different language? Then you got MoonLoader. One most notable feature is the error handling. CLEO script errors? Goodbye to current game session. MoonLoader script errors? Error is logged then script execution stopped.

As a side note, if you only want to dump TXD texture to PNG or TGA, FFmpeg supports TXD file and can transcode it to supported image formats (but you can't create TXD sadly).

Sunday, October 28, 2018

Music quality is associated with file size. True?

People often say that "if the audio size is big, that means the quality is nice". Actually, not really. Let me tell you one true story.

One day when I was surfing with my Facebook, there's Romeo and Cinderella movie video which was performed by Poppin'Party. It was part of BanG Dream x VOCALOID collaboration, so yeah it's normal, except the movie video uses the full version of the song. With my element inspection & dev console magic, I managed to get the audio file (it's not really legal, forgive me). The audio is encoded using AAC-HE encoding, with 48Kbps bitrate and frequency at 44100/22050Hz. The resulting file size is ~1.6MB. If you think the quality is bad, keep reading the story below.

A few days later, one of my friend managed to get 320Kbps MP3 version of the song, and posted a screenshot about it. The screenshot display full artist name, title, and even cover art. However, as you may know, those can be easily edited with many free software (FFmpeg allows you to add artist name and title, and the cover art can be easily obtained from clickable "Poppin'Party" above). When I see his post, I was skeptical about it. so I managed to ask if he can send me the audio file for analysis purpose, in exchange with the AAC-HE version that I have. He lend me the MP3 version, which has size around ~10MB (which is normal for 320Kbps MP3), and I also lend him my AAC-HE version, which is ~1.6MB (almost 10x smaller). He can't tell the difference between his MP3 version and my AAC-HE version sadly, but he only says that mine has lower audio volume (which is not really related).

Now, it's time to put it to FL Studio's "Wave Candy" plugin. First, I set up FL Studio to listen to Stereo Mix (it's rare to find laptop/PC with this feature). First test is to use my AAC-HE version that I got from Facebook. The result? bit surprising. I already knew that AAC-HE has better quality for lower bitrate as low as 32Kbps. The AAC-HE version has ~16KHz fieldity (better band preservation). Then when I test the MP3 file, my guess is correct, the band preservation is only ~12KHZ, way lower than AAC-HE version that I got. I also can differentiate the audio quality with my ear alone, and my ear tells me that the MP3 audio quality is lower than the AAC-HE version.

From that, we can conclude that smaller audio file size doesn't mean lower quality. It depends on encoding used and how lossy it was before re-encoded. Because, as you may know, re-encoding lossy audio degrades the quality even and even more. You also can't trust the file size alone because, say I re-encode that Romeo and Cinderella song (AAC-HE version) to WAV 44100Hz (which yields to ~40MB), I won't gain anything, and in fact I only waste drive space (it will only have ~16KHz fieldity). The quality is then exactly same as AAC-HE version, and if I re-encode the WAV to AAC-HE again, the quality won't be same as the first one.

Then, back to the title of this post. Music quality is associated with file size. True? The answer is FALSE.

Friday, October 19, 2018

Girls Band Party! PICO Episode 16 Bodyswap Explained.

Before you read the rest of the post, please watch the video.

Okay based on that video, we can see that all members of Poppin'Party is switching bodies to each other. This blog post will explain who swap to which one.

  1. Duration 0:41, we can see Kasumi's sister, and Arisa saying "I'm home, Acchan" (Kasumi call her sister Acchan). Then Kasumi's sister asking "Ichigaya-san where's my sister" then Arisa says "What are you talking about?". Also at duration 1:03, Arisa replies "This is Kasumi". So, we can then conclude that Kasumi is in Arisa's body.
  2. The following duration (1:03), we can see it's not usual Rimi expression (she's usually shy), which is more similar to Arisa. Also in duration 1:59, Rimi is asking Arisa (which is Kasumi inside Arisa body) to bring her body back. From this, we can conclude that Arisa is in Rimi's body.
  3. Duration 2:02, Saaya asking Rimi to stop bang her head to Arisa followed by "Stop! That's my body!". For some people, it may sounds ambiguous, considering Kasumi is in Arisa's body now, and Arisa is in Rimi's body, also with different face expression of Saaya, I think we agree that Rimi is in Saaya's body.
  4. The rest (Tae and Saaya) is bit hard. We'll start with Tae first. Where's she? Let's take a look at the beginning, duration 0:15. Remember Tae's expression correctly here. Then seek to duration 2:04 (if possible, slow the video down to 25% to see correctly). You can see Kasumi's expression is similar to Tae one in duration 0:15. Based on this, I think we can agree that Tae is in Kasumi's body.
  5. Then, where's Saaya? I think it's clear which body is Saaya. Kasumi is in Arisa's body, Arisa is in Rimi's body, Rimi is in Saaya's body, Tae is in Kasumi's body, then Saaya must be in Tae's body!
I think that's it. If you still don't understand, please see this table.

Body Kasumi Tae Rimi Saaya Arisa
Soul Tae Saaya Arisa Rimi Kasumi

When you promote your band, make sure to promote your band at pet shop at first.

Friday, June 29, 2018

LÖVE Optimization Tips

It’s common for people to write game and then “damn it performs very slow”, or “ok this game is fast enough” then when testing in slower hardware it complains “damn it’s slow af”. So these points will help you to optimize your game for more performance. This assume you’re using LOVE with LuaJIT as LuaVM. If you have different configuration (like LOVE with Lua 5.2 or with Lua 5.1), some points in here may not relevant.

Note that these things may be considered as micro-optimization in desktop PC, but it’s noticeable in mobile.

Move your game computation in love.update, and keep love.draw solely for rendering.


Okay let’s see why. First, math computation in LuaJIT are compiled, which mean it’s fast. Second, calls to love.graphics api in LuaJIT are not compiled and break the code around that area to be not compiled. So instead of

function love.draw()  
 var1 = var1 + 20 -- this computation is done in LuaJIT interpreter  
 -- because calls to love.graphics.draw below can't be compiled  
 love.graphics.draw(image, var1 * 0.02, 0)  
end

Do it like this.

function love.update()
 -- this computation is done in machine code because
 -- it's compiled.
 var1 = var1 + 20 * 0.02 -- LuaJIT will optimize 20*0.02 to 0.4  
end

function love.draw()
 love.graphics.draw(image, var1, 0)  
end

A note: This is less relevant for mobile (iOS and Android), as JIT compiler is disabled by default.

Use SpriteBatch, especially if it’s static.


Admit it. All calls to SpriteBatch object method will be never compiled. As of LOVE 11.0, there’s a feature called autobatching. That’s it. It automatically batches your draws, much like SpriteBatch with “dynamic” and “stream” mode.

Now, why use SpriteBatch for static batches? That’s because it’s better to call love.graphics.draw once to draw many things than calling love.graphics.draw to draw each thing. The former saves CPU due to Lua to C API call overhead, allowing your game to run faster.

Only use pairs when you don’t have a choice.


Even with LuaJIT 2.1 trace stitching feature (allows uncompiled function like love.graphics.draw not to break JIT compilation), pairs still breaks JIT compilation because it uses uncompiled bytecode. You can check what things that are compiled or not in here.

If you really need to index by things other than number, store the key in numered array, index that, then use the value to index the actual table. Then you can use ipairs or simple for loop to iterate the table (both are compiled). Something like this.

myTableKey = {"key1", "key2","key3"}
myTableValue = {key1 = "Hello", key2 = "World", key3 = io.write}
for i, v in ipairs(myTableKey) do
 local value = myTableValue[v]
 -- do something with `value`.
 -- It can be either "Hello", "World", or `io.write` function.
end

Oh also pairs doesn’t guarantee the order they’re defined. When you run pairs in myTableValue above, key3 might be shown first, then key1, then key2.

Use io.write for simple debugging, not print.


print is not compiled, but io.write is. The downside of io.write is that you’ll lose of automatically separated values and object-to-string conversion feature. If you don’t care about this, you can use print. Well, this is solely a preference. If you need the full power of print, then use print. For very simple variable printing, use io.write.

Never use FFI for computation optimization purpose.


Very true if you’re targetting mobile devices. Check out this chat I took from LOVE Discord server.
A: Will vector-light be faster than brinevector?
B: Depends. Brinevector is faster, but only on desktop. It’s terribly slow on mobile. Vector ligh (hump?) Is faster, but the code is messy since thats only for vector operations
A: if I made an FFI version of vector-light that was just pure functions, it would be faster right?
That’s where the problem begins. Once you step into FFI, your phone will start to cry when running your code.
Wait, based on benchmark, FFI is faster than plain Lua table.
That kind of argument doesn’t work once you have JIT compiler disabled. I’ve tried to benchmark it that in most cases FFI is ~20x slower than plain Lua when JIT compiler is disabled. Then what’s the matter for mobile devices? Take a look at first tips above. Mobile devices have JIT compiler disabled by default, which means slow FFI.

Well, you shouldn’t use FFI in mobile devices at all anyway. Starting from Android 7, any calls to ffi.load will just fail due to changes in Android 7 dlopen and dlsym which restrict those function greatly that it makes ffi.load cease to function. And in iOS, I don’t see the point of using FFI as all things must be statically compiled.

Switch the JIT compiler before love.conf is called in conf.lua


You may ask yourself. Why? The answer is, LOVE utilize some FFI call optimizations when it detect it runs with JIT compiler enabled, mainly in love.sound and love.image modules, and the detection is done before main.lua is loaded. Some people did this wrong by turning the JIT compiler on/off in main.lua, which can lead to slower performance.

Conclusion


Those kind of optimization is mostly helps in CPU optimization. For GPU, you need to reduce drawcalls, reduce canvas usage (or don’t use it at all), and use compressed texture. If it’s still slower, but the CPU stays low, that indicates you’re fillrate-limited, in which case, any optimization in here doesn’t really help you at all.

Note that I may still wrong, so if you have something to add, or something is invalid in above tips, just comment below to discuss it.

'What was the original problem you were trying to fix?' 'Well, I noticed one of the tools I was using had an inefficiency that was wasting my time.'

Also, did I also mention that JIT compiler for ARM64 is unstable?

Monday, June 18, 2018

Firefox DNS-over-HTTPS

TL;DR: Firefox DoH is buggy atm. Hopefully it's usable in Firefox 62, or at least Firefox 61.


Sunday, June 10, 2018

Monday, May 14, 2018

SIF User Collision

There are limits in SIF about maximum user before problem start to arise. So let's start to discuss about it.

Have you wonder about invite codes? The code that you use to add someone as friend? This one can only hold up to 999999937 unique code (and actually only can hold 999999937 users), or it will start to collide with first user in the SIF, second user, and so on. Basically, that invite code is calculated using this formula (source):

$$ U_ic=(U_id \times 805306357) \mod{999999937} $$

Where:
  • \( U_ic \) is the invite code
  • \( U_id \) is the user id
Next is user credentials. Believe it or not, your account credentials is stored in keychain. It's in plaintext by default in Android (GameEngineActivity.xml) or in iOS, it's built-in keychain storage. This credentials consist of random characters in UUID form (but actually is not really UUID-compilant) and random 128 hexadecimal character. The UUID also only uses hexadecimal characters, and generated randomly when user is creating account. So the chance of this one collide is 2.935x10^-37 %. How long is it? Very very long, that waiting for the 999999937 invite code limit took less time than waiting this one to collide.

Well, all those thing are depends on environment, bit rot, and the current phase of moon. Maybe when we observe the moon phase to decide when to fasting, that UUID just collide each other, damn.

Saturday, May 5, 2018

Keyboard

Tae Hanazono asking a question in the other day.
Is a keyboard a board "of" keys, or a board "for" keys?
In music context (piano, synthesizer, computer), it will be "board of keys".


First, I take a look at definition of keyboard, which is "A set of keys on a piano or similar musical instrument.". The use of "of" word in "A set of keys" makes this clear that for piano, a keyboard is a board of keys. Board of keys also makes sense for computer keyboard if that's not enough for you.

So, what exactly is "board for keys" then? Board for keys is actually key hook (or key hanger, you name it). In Indonesia language, "board for keys" means "papan untuk kunci" and "board of keys" means "papan kunci". Since "papan kunci" doesn't fully makes sense, so it should be "papan tuts" in music context. "ivory" or "key" in English means "tuts" in Indonesia for music context. Unfortunately in English, key for keyboard and key for opening a door is both called "key" (or "keys" if there are more than 1 key).

A simple tip: If you search "board of keys" and "board for keys" in Google (with the double quote), you'll get different result. The former will be more music-related and the latter will be key hook. Honestly, I would say this is situation where the use of "for" and "of" can cause significant context differences.

As a Tae Hanazono fan, this question really surprise me and bothers me at same time.

Tuesday, May 1, 2018

SIF Note Handling: Observation & Accuracy.

Recently some people complain about Live Simulator: 2 timing. I exactly remember that Live Simulator: 2 code the note accuracy based on my previous article regarding SIF note handling, but they say it doesn't seems right, so I decide to test it in Live Simulator: 2 and SIF. The difference were significant but not very. I decide to record my SIF note tapping observation video below.

(Sorry for the 30 FPS video; Google Play Games just can't record thing in 60 FPS, yet).
I used 1.6second note speed (note velocity = 250px/s) and WHITE FIRST LOVE (Normal) beatmap.

As you may know, SIF use this timing table


From now on, ignore the "C" column and focus on "A" and "B" column. However the statement "For swing, Perfect = Great accuracy; Great = Good accuracy" still applies in this context.

Probably you were wondering, how big is 128 pixel? The short answer is, the tap icon diameter. Tap icon diameter is roughly 128 pixels (there are some padding added but actual size in-game files is 128x128px).

One example, when I tap the 2nd note in that video, I time it to be slightly inside idol icon radius and getting good. It's actually true, because the tap icon diameter is 128px, and as basic geometry math tells you that circle radius is half of the diameter, so the tap icon radius is 64 pixels. If you look at the table, 40-64 pixel result in "Good" judgement.

Another example for long note case, when I started the long note in 3rd note, but when I release it, it result in miss. It may looks like should fall in "Bad" range (because the distance is <128px) but if you look it correctly in table above, 112-128 range result in "Miss" judgement.

Now what's the use of that "128px" in Miss row? That 128 value is only used for tapping the note, not for release. Look at 0:37 and you'll see long note which I tap and result in "Bad" judgement (and if you observe carefully, the distance is actually 128px). But why? The answer is, to prevent you tapping the note too early. Say, you tap the note in 200px distance, but since maximum distance is 128px, the game doesn't register "tap" to the note.

What if you missed the note completely? In 0:54, I missed a note in the left side, but it's count as "Miss" when the distance of the note is 128px. In this case, we can conclude that distance 112px or higher result in "Bad" for single note and result in "Miss" for long note trail.

That observation assume 0 timing offset. If timing offset start into play, things start get trickier. I haven't observe when timing offset comes into play, so the rest of this writing is entirely subjective. Winshley said from reddit that "it's almost impossible to get late "Bad" tap with -50 timing offset". That means the note judgement tap range is shifted, but not the 128px range. If my calculation is right, the late "Bad" tap (64-112px range) in -50 timing offset, is 114-162px. However, the maximum limit is 128px, so the range is actually 114-128px. There's only 14px gap between, and with note speed of 0.5 second (to be exact, 0.571428 second), it will be impossible to get late "Bad" tap at all and you'll either get late "Good" or late "Miss".

Because this is based on observation, real-life observation, that means my previous article is invalid (which is completely theoretical). I'll plan to fix all of this mess in Live Simulator: 2 and add SIF timing offset instead of global offset which shifts the whole beatmap time.

At last, I've played WHITE FIRST LOVE song in 5 days a row and I'm not even getting bored of it.

Wednesday, April 4, 2018

Girls Band Party & New Phone

When the first time BanG Dream Girls Band Party announce the English version is coming in March, I looked up for a new phone candidate. My budget is not much, just uh, 3 million IDR and this will be used for long-term fashion. Here's the criteria I'm looking for:
  • Camera doesn't have to be very good, but camera which stabilize when taking photos of text is needed.
  • It must comes with at least 64GB of internal storage. Some people says that it's too much, but trust me, having such big storage will come handy in the future.
  • Should be recent. 2016 release should be good.
  • OS must be Android. Bypassing region lock in Play Store with Android is easier than in iOS, and some of my games are region-locked. The Android version doesn't have to be too new, but Android 7.0 is preferred.
  • Must have at least 3GB of RAM. 
  • Must have fingerprint sensor (rear-mounted is preferred). I don't want to bash the power button.
  • CPU and GPU should be powerful enough. The CPU should be somehow comparable with Intel Atom Z3560. Snapdragon 6xx series chipset looks promising in this case.
  • Not to mention screen size. The maximum screen size is 5.5". Bigger than that and I won't able to hold it properly and/or tapping far notes in School Idol Festival.
  • Dual-SIM at least.
Inputting those criteria to GSMArena phone finder shows up some result, but decided to filer what I think looks best, and my impression on it:
  • SAMSUNG Galaxy S7 and it's variants. People reports playing School Idol Festival with SAMSUNG phone is bad as very old phone. People says the touchscreen is horrible and randomly drops notes, so no for this one. Not to mention it's very expensive.
  • OnePlus (any type). Not sure if it's in Indonesia, and I can't find any reports about running School Idol Festival in it. Expensive.
  • ASUS ZenFone 3 series. Same as OnePlus above, except it's available in Indonesia, and it's cheaper than OnePlus probably.
  • OPPO Series. No idea about running School Idol Festival there. Also it's expensive here and some people says the performance is awful.
  • Xiaomi Redmi Note 4. My friend use this one and strongly recommends me this one. It's cheap and suits what I needs, but someone reports playing School Idol Festival there is awful. My friend doesn't recommended the MediaTek version though.
  • Motorola Moto X4. Same as OnePlus above.
  • ZTE series. Same as OPPO above, but no idea about performance.
  • LG G5. I don't know why I put this here. Looks good, but only 32GB of internal storage.
  • Motorola Moto G5S+. Looks fit my criteria. I don't like the camera placement (can easily scratched) and the front-mounted fingerprint makes me think twice. Not to mention the audio is horrible according to GSMArena review.
  • And the last, Xiaomi Mi A1 (one that I choose; not the 5X variant). Very similar to Moto G5S+ above, and literally someone in SIF Discord confirms the phone to be work with SIF. Doesn't come with Radio FM but it actually has receiver. Cheaper than Moto G5S+ and better, a bit.
(Note that impression above are subjective!)
And then somewhere in early March I got my Mi A1. Testing time.

School Idol Festival runs fine. No stutters. No lags. Performs exactly same as my previous phone, ASUS ZenFone 2 (which uses Intel Atom Z3560). Testing the Japanese version of Girls Band Party, unfortunately lags out when doing Live! and when reading some stories (probably there are too many Live2D objects to load and render). My older ZenFone 2 still performs way better, probably because Girls Band Party is way more optimized in x86 platform than ARM. But since v2.0.0 of the game, the lag is significantly minimal now. Only lags a bit when doing multi live with LIVE FEVER (all effects enabled). Not sure how my ZenFone 2 handles v2.0.0 now, probably without any lag at all.

Battery life is good. You know what, the first time I extensively testing this phone is by playing SIF and reaching the top 1100. Long run test (2 hours) for reaching 25k event points only consume 20% of battery, which is I can consider very good, although not as good as Redmi Note 4's 4100mAh battery, but I can say it's acceptable.

For the Global version of Girls Band Party, surprisingly, less laggy than pre-v2.0.0 Japanese version of the game. In math, the lagness will be notated as "v2.0.0 JP < v1.1 WW < pre-v2.0.0 JP" where v2.0.0 JP is the most less lag.

To end this up, Girls Band Party performs better in x86 phones (BLACK SHOUTsly ZenFone 2), but x86 phones are horrible in terms of battery life, and it gets really hot in short time.

Monday, March 26, 2018

Sunday, March 25, 2018

HonokaMiku Short Story

This is what the current state of SIF WW now, tons of cheaters entering the score match, because me. Yes, me! Here's a short story why.

It's all start from October 21st, 2015, where the disaster started. It was me developing an SIF files decrypter a few years ago. Probably the first one which become open source, based on my first successful attempt to reverse engineer program (read: SIF). I was hoping this tool can be good for datamining the game files to make predictions more and more accurate (PS: it's Eli Ayase birthday; I choose that specific date to release my decrypter).

Times has passed, like 1 year and 2 years. My decrypter received good support of being able to dump all cards, game database, and even almost-functional SIF private server at that time. It helps me alot understanding the game mechanics, as well as helping other people dumping card images from their rooted phone. Not to add my SIF live simulator is mostly based on what I found in SIF game code.

Somewhere in the early 2018, someone asked me (from Indonesia), via my Live Simulator: 2 Facebook page, how to decrypt Lua files with HonokaMiku. This is probably where things start to be in situation like today. I'm sorry, I wasn't aware at that time where he used it to create modded game, so I tell him how to do things like "unluac", but not all.

A few days later there was a news that there's Indonesian hacker which reach the top 10 event song ranking, and my reaction like "Man WTF?". After some investigating, he post it in Android modding forums. He even write my internet name (MikuAuahDark) in his post because he used HonokaMiku to create his modified APK and I'm angry because that, so I asked him to apologize and remove my name. Yet, only for some time, then he's strikes again.

Today, most dataminers now uses libhonoka, another decrypter which I write entirely in C, unlike HonokaMiku which is written in C++. It's also faster and better than HonokaMiku, but I swear I'll not give libhonoka to anyone unless who I trusted. It can compromise the game in point where changing encryption is not an option.

Cheaters (mostly Indonesians, or I'm just gonna call them Indons) are now widely spreaded. KLab WW (or should I call it Kebab?) tried to ban accounts for cheating, multiple times, or at every end of event, by nuking suspicious event ranking. But Caraxian said (one of dataminer) that banned accounts in SIF WW can be retrieved back if you have the passcode. Man for real?

To the cheaters, I suggest you not to use the modified APK for cheating, but use something like automated scripts which can save your time reaching the ranking. If you know about "The Friendly Hacker", you should get the idea. The downside is, you need to know how to authenticate with the server (which is stored in binary, and you have to read tons of assembly code for that).

(FYI: The same person, the cheater, actually tries to hack in SIF JP, but got real banned a few minutes later lol).

I've tried many ways to stop them all, including requesting KLab WW (in this context, Kebab) to takedown my decrypter and it's forks for good. I've tried multiple times, but they just didn't do anything. If KLab WW (Kebab) staff read this, please, please, just request GitHub to takedown that repository, along with all of it's forks, and switch your game encryption immediately. If you have better idea how to stop those cheaters, just comment below and I'll send it to Kebab.

TL;DR: SIF WW is compromised because HonokaMiku, and KLab WW (again, Kebab) haven't do anything to strengthen their game security.

It was the point where I was not aware, but now, I'm aware about this nice quote.

"With Great Power Comes Great Responsibility" - Ben Parker

Friday, March 16, 2018

Fixing Image Quality of SIF Cards: RGBA4444 vs. RGB565

Some people discussing about SIF cards image quality being sucks since New Year Kanan UR. They said somehow KLab change the dithering method used in cards and always uses RGBA4444. I argue that RGB565 for clean UR card are better and actually makes sense (clean UR card doesn't need transparency, but "navi"[1] requires transparency).

Here's the original card image, to be used for comparison (the latest Ruby UR card paired with Yoshiko as I'm writing this):

1489u-orig.png

You can notice the grainy pictures, or noise caused by dithering and RGBA4444 color loss. I start to think that something wrong about the dithering algorithm. Here's the version with selective gaussian blur applied (someone in the discussion do the blur):

1489u-sgb.png

Selective gaussian blur does the job looks like, but I said, that waifu2x can achieve something better, but then he doesn't agree and says that waifu2x destroys the edges and some parts of the image. Well, I'm just gonna do it anyway. I tried many different methods, like noise level 1, level 2, noise level 2 then 2x scale then 50% downresize, but I found noise level 1 is already doing the job pretty well and noise level 2 destroys some details as he says before. Here's the image result:

1489u-w2x-noise1.png

Ok it looks good now, so let's apply RGBA4444 conversion and dithering to the noise removed image. Unfortunately, ImageMagick can't do RGBA4444 conversion (and any 16bit RGB color conversion) and specify dithering at same time, so I have to generate the necessary color table (RGBA4444 and RGBA565 for our purpose) and use -remap option in ImageMagick later. Here's the command that I use to generate the color table:

C:\Users\MikuAuahDark\Pictures\1489>magick convert -size 256x256 xc:none -channel R -fx "((i*w+j)&15)/15.0" -channel G -fx "(((i*w+j)>>4)&15)/15.0" -channel B -fx "(((i*w+j)>>8)&15)/15.0" -channel A -fx "(((i*w+j)>>12)&15)/15.0" rgba4444.png
C:\Users\MikuAuahDark\Pictures\1489>magick convert -size 256x256 xc:white -channel R -fx "((i*w+j)&31)/31.0" -channel G -fx "(((i*w+j)>>5)&63)/63.0" -channel B -fx "(((i*w+j)>>11)&31)/31.0" rgb565.png

Now we can tell ImageMagick to use our color table:

C:\Users\MikuAuahDark\Pictures\1489>magick convert 1489u-w2x-noise1.png -dither none -remap rgba4444.png 1489u-w2x-noise1-rgba444none.png
C:\Users\MikuAuahDark\Pictures\1489>magick convert 1489u-w2x-noise1.png -dither riemersma -remap rgba4444.png 1489u-w2x-noise1-rgba444rie.png
C:\Users\MikuAuahDark\Pictures\1489>magick convert 1489u-w2x-noise1.png -dither floydsteinberg -remap rgba4444.png 1489u-w2x-noise1-rgba444fstein.png

And here's the result:

1489u-w2x-noise1-rgba444none.png -dither none 1489u-w2x-noise1-rgba444rie.png -dither riemersma 1489u-w2x-noise1-rgba444fstein.png -dither floydsteinberg

With -dither none, the color difference is very noticeable, while using -dither riemersma or -dither floydsteinberg produces similar result to dithering algorithm that KLab uses (if you look closely, KLab dithering looks better a bit). So it's the RGBA4444 that causing image quality problems. Ok now let's switch to RGB565.

magick convert 1489u-w2x-noise1.png -dither none -remap rgb565.png 1489u-w2x-noise1-rgb565none.png
magick convert 1489u-w2x-noise1.png -dither riemersma -remap rgb565.png 1489u-w2x-noise1-rgb565rie.png
magick convert 1489u-w2x-noise1.png -dither floydsteinberg -remap rgb565.png 1489u-w2x-noise1-rgb565fstein.png

1489u-w2x-noise1-rgb565none.png -dither none 1489u-w2x-noise1-rgb565rie.png -dither riemersma 1489u-w2x-noise1-rgb565fstein.png -dither floydsteinberg

Oh! The RGB565 variant looks way better than ones encoded with RGBA4444. My argument is correct that RGB565 is certainly better for clean UR cards rather than RGBA4444. But the problem is, the RGB565 variant increase twice as RGBA4444 variant and that's just the PNG representation, so let's try to simulate how it's stored in SIF game engine texture bank format and compare their size.

SIF texture bank can store the raw pixel data in variety of different pixel formats[2]. For RGB, there are RGBA5551, RGBA4444, RGB565, and RGBA8888 (RGBA8888 is the usual pixel formats used in images and Photoshop RGB/8). The raw pixel data can be stored uncompressed or compressed with zlib[3], PVR, ETC, or other compression formats supported by the GPU, but most of the time KLab just uses zlib compression, so we use zlib compression. In this example, we picked -dither floydsteinberg with RGB565 and RGBA4444 variant. For sake of tool completeness, I used Linux WSL environment since I don't know how to specify compression level in openssl zlib.

Unfortunately, FFmpeg nor ImageMagick can't output to raw RGBA4444, so I write my own script to do it later in WSL. Here's the Lua script:

#!/usr/bin/env luajit
-- Expected RGBA8888 stdin

local ffi = require("ffi")
local w = assert(tonumber(arg[1]))
local h = assert(tonumber(arg[2]))

local rgba4444 = ffi.new("uint16_t[?]", w*h)
local rgbstruct = ffi.typeof("union {struct {uint8_t r,g,b,a;} rgba; uint8_t raw[4];}")

for i = 1, w*h do
    local d = rgbstruct {raw = io.read(4)}
    -- Remember FFI arrays are 0-based index
    local r = bit.lshift(bit.rshift(d.rgba.r, 4), 12)
    local g = bit.lshift(bit.rshift(d.rgba.g, 4), 8)
    local b = bit.lshift(bit.rshift(d.rgba.b, 4), 4)
    local a = bit.rshift(d.rgba.a, 4)
    rgba4444[i-1] = bit.bor(bit.bor(r, g), bit.bor(b, a))
end

io.write(ffi.string(rgba4444, w*h*ffi.sizeof("uint16_t")))
os.exit(0)

And here's the command I'm using (have to rename WSL bash.exe so it doesn't clash with MSYS/Cygwin bash):

C:\Users\MikuAuahDark\Pictures\1489>env winbash
mikuauahdark@Xyrillia-20166:/mnt/c/Users/MikuAuahDark/Pictures/1489$ ffmpeg -i 1489u-w2x-noise1-rgb565fstein.png -pix_fmt rgb565 -f rawvideo 1489u-w2x-noise1-fstein.rgb565
mikuauahdark@Xyrillia-20166:/mnt/c/Users/MikuAuahDark/Pictures/1489$ ffmpeg -i 1489u-w2x-noise1-rgba444fstein.png -pix_fmt -f rawvideo - | ./rgba4444.lua 512 720 > 1489u-w2x-noise1-fstein.rgba4444
mikuauahdark@Xyrillia-20166:/mnt/c/Users/MikuAuahDark/Pictures/1489$ cat 1489u-w2x-noise1-fstein.rgba4444 | pigz -z -11 > test-1489u-rgba4444.zz
mikuauahdark@Xyrillia-20166:/mnt/c/Users/MikuAuahDark/Pictures/1489$ cat 1489u-w2x-noise1-fstein.rgb565 | pigz -z -11 > test-1489u-rgb565.zz

With just this information, we can estimate the texture bank size. RGBA4444 variant size (compressed) is 245965 bytes, and the RGB565 variant size (compressed) is 399097 bytes. That's 162% size increase. Now let's calculate how much the additional size increase in SIF data if RGB565 is used. We also need to assume average 165% size increase, avg. 500KB of each card file (in RGBA4444), and all clean UR cards are encoded in RGBA4444 (actually, there are already cards that are encoded to RGB565 before, but let's assume all images were encoded to RGBA4444 at the moment to simplify it).

As of writing, School Idol Tomodachi says there are 86 SSR and 234 UR cards. Not all cards have unidolized form and only have idolized variant, but we still can get estimation of such cards from School Idol Tomodachi too, and it says there are no SSR and 79 UR cards with only idolized form[4], which in total there are 561 cards[5]. The cards total size when encoded is:

RGBA4444 Cards = 561 Cards * 500 KB = 274 MB
RGB565 Cards = RGBA4444 Cards (274 MB) * 165% = 452 MB
Size Increase = |274 MB - 452 MB| = 178MB

The size increase is ~178MB. How big is it? BanG Dream Girls Band Party voice size is ~150MB if I remember. Live Simulator: 2 APK size is 23MB, equivalent to 8 Live Simulator: 2 in your Android phone. 178MB is around twice of SIF install data size (the one preloaded into your phone when you started SIF for the first time). Size increase of 178MB should be acceptable, so I think it's gonna worth it if it's re-encoded to RGB565. The problem is that you'll have to download ~452MB of cards if this happends.


The conclusion is: I don't see reason why KLab uses RGBA4444 for clean UR cards except to reduce size, but the quality-size tradeoff is simply unbalanced (as in UNBALANCED LOVE), so KLab should change the pixel format for their clean UR cards from RGBA4444 to RGB565 to increase the image quality, and then use Genki Zenkai dither algorithm with DAY! DAY! DAY! recipe to decrease the size a bit.


[1]: "navi" means the transparent variant of SIF cards, the one used in your home SIF menu. Dunno if KLab is having joke with Avatar, but of course KLab doesn't know anything about the Legend of Aang.

[2]: https://github.com/MikuAuahDark/Itsudemo/blob/master/TEXB_stream_format.txt#L29

[3]: https://github.com/MikuAuahDark/Itsudemo/blob/master/src/TEXBLoad.cpp#L195

[4]: https://schoolido.lu/cards/?search=&rarity=UR&attribute=&is_promo=on and https://schoolido.lu/cards/?search=&rarity=SSR&attribute=&is_promo=on

[5]: IdolizedMultipler*(SSR TotalCard+UR TotalCard)-(SSR PromoCard+UR PromoCard)

I write the post in markdown and converted it to HTML, so I'm sorry if there are problems.

Tuesday, March 13, 2018

Some Q&A Part 1

Someone send me an E-Mail.



how many hours a day you code?
what was the hardes part/feature you have programmed?
who are your best girls muse/aqours and worst?
how long till delete beatmap function and restart option in pause
menu? ༼ ◕_◕ ༽
what are the ideas for the game that you trashed into the bin - were
not implemented but could be cool?
name 3 reasons why should i come to indonesia?

The question looks like mixing alot things, like a personal question, Live Simulator: 2 questions, and Love Live! related question. You may wonder why, but recently I'm opening some kind of AMA related to technical thing about SIF. Here's my tweet mentioned about it (it's also pinned in my Twitter profile). Well, let's answer the question one by one.

how many hours a day you code?
Depends on my mood, time I have, school, and irl things. Most of the time I code 3 hours a day. 7 PM to 10 PM (UTC+8, no DST), but sometimes I code even at 8 hours a day. It all depends on many factors that you may can't think of. In short, my coding time is inconsistent.

 what was the hardes part/feature you have programmed?
 Hardest part? There are some. The first one is this, which turns out to be the base of all SIF WW cheats out there, sad. Second is FFmpeg video loader (FFX) for my Live Simulator: 2. Basically FFX loads any audio/video formats supported by current loaded FFmpeg (libav) libraries and you can use it as video storyboard for your beatmap in Live Simulator: 2. And the last is GUI. Yet, I still can't think a way how to structure GUI codes correctly. I always take too many assumptions when coding it, like optimization, multitouch support, textinput, and more.

who are your best girls muse/aqours and worst?
Myus (that's how I pronounce it. Too sad I can't type greek mu in my laptop) best girl, Honoka Kousaka. For worst girl, probably none, but I found Maki Nishikino is bit annoying (sorry Maki fan).

I prefer love things than hating it, but it's totally different for Aqours. I don't really like Aqours for some reason (and even angry to myself if I accidentally forgot to switch from Aqours mode to Myus mode before exiting SIF). But since you asked it, best girl for Aqours is Riko Sakurauchi, and worst girl is Ruby Kurosawa (sorry Ruby fan), because her voice anoys me a bit.

how long till delete beatmap function and restart option in pause
menu?

Not sure what you mean, but if you mean Live Simulator: 2, then it should arrive in v2.2 :P 

Update: It ariived in 3.0

what are the ideas for the game that you trashed into the bin - were
not implemented but could be cool?

Not sure what game, but if you mean Live Simulator: 2, then probably a full team simulation. This thing is probably arrive in my next project, FiLive! (rewritten Live Simulator: 2 to C++), but probably I'll start FiLive! development once Live Simulator: 2 v3.0 hits, which is in a few years in the future :P

name 3 reasons why should i come to indonesia?

There are plenty of reasons. But of course, not to meet me specially, since I'm introvert. Simple Google search result shows me article from telegraph.co.uk, which lists 15 reasons why you should visit Indonesia. So I'm just pick best reasons: Komodo Island, Borobudur Temple, and the diving to see corals, quoted from the same article, "One of the best ways to explore it is on a liveaboard boat around the Raja Ampat Islands in Indonesia's West Papua province.".

I suggest you to visit Bali first. I also have plans to Bali in June, so if we're somehow meet each other, we'll start Time Lapse by Poppin' Party (the song I'm listening in loop while writing this).

EDIT: Updated FFmpeg video loader link to point to LS2 v2.x