On Telegram crashing under webview

Problem Statement

Telegram desktop client crashes with a SIGABRT whenever it tries to render a webpage with QT webview. This seems to be a problem unique to Wayland-based system running on proprietary NVIDIA userspace GL/EGL drivers. Telegram upstream is aware of the issue but isn’t going to find the root cause with their time.

At time of writing this is reproducible with the following versions:

  • Telegram desktop 6.8.2
  • NVIDIA egl-wayland 1.1.21
  • NVIDIA drivers 610.43.02
  • QT6 6.11.1

OriginCode has already opened a ticket with egl-wayland over a month ago, but so far the ticket doesn’t seem to have received any replies. No choice but to take a stab myself.

Debug working notes

First look at a previous core dump

Systemd core dumper managed to capture telegram’s dead memory space. Loading the core in GDB revealed a assertion error inside wlEglAcquireDisplay.

... [telegram aborting itself after an assertion failure]
#5 0x00007ffff066e4e5 in __assert_fail (assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=0x55555d94a1a0 "`\263w]UU") at assert.c:127
#6  0x00007fffcd421e1d in wlExternalApiLock () at ../src/wayland-thread.c:87
#7  0x00007fffcd4226c1 in wlEglAcquireDisplay (dpy=dpy@entry=0x55555d94a1a0) at ../src/wayland-egldisplay.c:1469
#8  0x00007fffcd4234dc in wlEglGetInternalHandleExport (dpy=<optimized out>, type=<optimized out>, handle=0x55555d94a1a0) at ../src/wayland-eglhandle.c:186
#9  0x00007fffc8331081 in ??? () at /usr/lib/libEGL_nvidia.so.0
#10 0x00007fffc82d62ce in ??? () at /usr/lib/libEGL_nvidia.so.0
#11 0x00007fffcd4295e3 in wlEglCreateStreamAttribHook (dpy=0x55555d94a1a0, attribs=0x7fffffffd150) at ../src/wayland-eglstream.c:200
#12 0x00007fffc83359e3 in ??? () at /usr/lib/libEGL_nvidia.so.0
#13 0x00007fffc82d6321 in ??? () at /usr/lib/libEGL_nvidia.so.0
#14 0x00007fffcd45d1ab in WaylandEglClientBuffer::setCommitted(QRegion&) () at /usr/lib/libQt6WaylandEglCompositorHwIntegration.so.6
#15 0x00007ffff30dc178 in QWaylandSurfacePrivate::surface_commit(QtWaylandServer::wl_surface::Resource*) () at /usr/lib/libQt6WaylandCompositor.so.6
... [A whole bunch of normal looking QT stuff. Presumed unrelated.]

Looking at the offending code for wlEglAcquireDisplay:

int wlExternalApiLock(void)
{
    if (pthread_once(&wlMutexOnceControl, wlExternalApiInitializeLock)) {
        assert(!"pthread once failed");
        return -1;
    }

    if (!wlMutexInitialized || pthread_mutex_lock(&wlMutex)) {
        assert(!"failed to lock pthread mutex");
        return -1;
    }

    return 0;
}

The whether-mutex-is-initialized flag shows no weird signs, but the very mutex it’s trying to acquire is already locked.

(gdb) print wlMutex
$1 = {__data = {__lock = 1, __count = 0, __owner = 530834, __nusers = 1, __kind = 2, __spins = 0, __elision = 0, __list = {__prev = 0x0, __next = 0x0}}, 
  __size = "\001\000\000\000\000\000\000\000\222\031\b\000\001\000\000\000\002", '\000' <repeats 22 times>, __align = 1}
(gdb) pipe info threads | grep 530834
* 1    Thread 0x7fffe86d3ac0 (LWP 530834) __pthread_kill_implementation (threadid=<optimized out>, signo=signo@entry=6, no_tid=no_tid@entry=0) at pthread_kill.c:44

The current thread already locked this mutex, but is somehow attempting to lock it again. NVIDIA egl-wayland initializes its mutex with PTHREAD_MUTEX_ERRORCHECK, and a deadlock situation thus caused locking to fail with EDEADLK, triggering the assert().

Now the remaining question becomes where and why the fuck egl-wayland attempts to lock the same lock twice.

Tracking lock usage in GDB with overengineered python

In a hindsight it should be rather obvious that the earlier locking must have happened somewhere in the upper stack frames, but my monkey brain is too comfortable writing python plugins for GDB both in my day-time job and when I struggle with Hollow Knight.

Loaded the thing into GDB, initialize the breakpoints tracking where locks are taken and released, and launch a new Telegram process.

INFO:__main__:Mutex 00007fffcd432380 unlocked by thread 530834
INFO:__main__:Mutex 00007fffcd432380 locked by thread 530834
INFO:__main__:Mutex 00007fffcd432380 unlocked by thread 530834
INFO:__main__:Mutex 00007fffcd432380 locked by thread 530834
INFO:__main__:Mutex 00007fffcd432380 locked by thread 530834
Telegram: ../src/wayland-thread.c:87: wlExternalApiLock: Assertion !"failed to lock pthread mutex" failed.

Thread 1 "Telegram" received signal SIGABRT, Aborted.

Two successive lock-acquire on the same thread. My script has saved backtraces each of these lock-acquire. Should reveal more details.

[BEGIN lock attempt -2]
#0  wlExternalApiLock () at ../src/wayland-thread.c:79
#1  0x00007fffcd4292b9 in wlEglCreateStreamAttribHook (dpy=0x55555d94a1a0, attribs=0x7fffffffd140) at ../src/wayland-eglstream.c:82
#2  0x00007fffc83359e3 in ??? () at /usr/lib/libEGL_nvidia.so.0
#3  0x00007fffc82d6321 in ??? () at /usr/lib/libEGL_nvidia.so.0
#4  0x00007fffcd45d1ab in WaylandEglClientBuffer::setCommitted(QRegion&) () at /usr/lib/libQt6WaylandEglCompositorHwIntegration.so.
...

[BEGIN lock attempt -1]
#0  wlExternalApiLock () at ../src/wayland-thread.c:79
#1  0x00007fffcd4226c1 in wlEglAcquireDisplay (dpy=dpy@entry=0x55555d94a1a0) at ../src/wayland-egldisplay.c:1469
#2  0x00007fffcd4234dc in wlEglGetInternalHandleExport (dpy=<optimized out>, type=<optimized out>, handle=0x55555d94a1a0) at ../src/wayland-eglhandle.c:186
#3  0x00007fffc8331081 in ??? () at /usr/lib/libEGL_nvidia.so.0
#4  0x00007fffc82d62ce in ??? () at /usr/lib/libEGL_nvidia.so.0
#5  0x00007fffcd4295e3 in wlEglCreateStreamAttribHook (dpy=0x55555d94a1a0, attribs=0x7fffffffd150) at ../src/wayland-eglstream.c:200
#6  0x00007fffc83359e3 in ??? () at /usr/lib/libEGL_nvidia.so.0
#7  0x00007fffc82d6321 in ??? () at /usr/lib/libEGL_nvidia.so.0
#8  0x00007fffcd45d1ab in WaylandEglClientBuffer::setCommitted(QRegion&) () at /usr/lib/libQt6WaylandEglCompositorHwIntegration.so.6
...
[Lock-acquire attempt fails here and causes SIGABRT]

An unknown piece of code from proprietary NVIDIA libEGL_nvidia.so has issued a call to wlEglCreateStreamAttribHook and it somehow caused itself a deadlock. I guess its time to read a bit more about this egl-wayland and its purpose. From a random search on Google, I landed on an NVIDIA presentation about libEGL on XDC2016. Looks like we have some wild pointer chasing and circular calling ahead of us.

egl-wayland API trampoline madness

The commentary inside the code base isn’t very enlightening, but together with those slides from NVIDIA I can kinda make an educated guess on the execution flow (after installing QT6 symbols).

[Qt6] WaylandEglClientBufferIntegrationPrivate::initEglStream
         |
         | 0. QT makes EGL create_stream_attrib_nv() call with an "external"
         |    display.
         v
[libEGL_nvidia.so]
         |
         | 1. Device EGL calls "external platform API".
         v
[libnvidia-egl-wayland.so.1.1.21]
wlEglCreateStreamAttribHook(display, attribute)
         |
======== | ===== wlExternalApiLock() held.
H        |
H        | 2. With the external API lock held, it fetches some display
H        |    metadata through the following API.
H        |    - wl_eglstream_display_get()
H        |    - wl_eglstream_display_get_stream()
H        |    With this information, it invokes the device EGL again
H        |    to actually create the stream.
H        v 
H data->egl.createStreamAttrib(display, modifed_attrib)
H [libEGL_nvidia.so] Function pointer set during driver init.
H        |
H        | 3. Calls wlEglGetInternalHandleExport through external API.
H        v
H [libnvidia-egl-wayland.so.1.1.21]
H wlEglGetInternalHandleExport(display, EGL_OBJECT_DISPLAY_KHR, display)
H        |
H        v
H [libnvidia-egl-wayland.so.1.1.21]
H wlEglAcquireDisplay(display)
H        |
H        | 4. Attempts to lock the external API lock again with
H        |    wlExternalApiLock().
H        |
H <========== PTHREAD_MUTEX_ERRORCHECK deadlock assertion tripped.

The wlExternalApiLock() logically protects the global linked list of displays against data race so that a display or an associated stream cannot just disappear. Functions that validate whether streams/displays are valid or change the list internally take this lock. But does it make sense to keep holding it across the “device-platform-device-platform” trampoline in step 4?

The Fix

Whatever downstream “platform” external EGL API it calls seem to do a fairly good job to make sure incoming pointers remain valid regardless of who (from application or from platform side) calls these APIs. Looks like the way out is to just immediately release the lock once step 2 in the chart is done, so that the remaining device EGL calls can take their own lock as needed.

diff --git a/src/wayland-eglstream.c b/src/wayland-eglstream.c
index 3c40a0d..611e773 100644
--- a/src/wayland-eglstream.c
+++ b/src/wayland-eglstream.c
@@ -89,15 +89,17 @@ EGLStreamKHR wlEglCreateStreamAttribHook(EGLDisplay dpy,
     }
 
     if (err != EGL_SUCCESS) {
-        goto fail;
+        goto fail_unlock;
     }
 
     wlStream = wl_eglstream_display_get_stream(wlStreamDpy, resource);
     if (wlStream == NULL) {
         err = EGL_BAD_ACCESS;
-        goto fail;
+        goto fail_unlock;
     }
 
+    wlExternalApiUnlock();
+
     if (wlStream->eglStream != EGL_NO_STREAM_KHR ||
         wlStream->handle == -1) {
         err = EGL_BAD_STREAM_KHR;
@@ -237,12 +239,11 @@ EGLStreamKHR wlEglCreateStreamAttribHook(EGLDisplay dpy,
     wlStream->eglStream = stream;
     wlStream->handle = -1;
 
-    wlExternalApiUnlock();
-
     return stream;
 
-fail:
+fail_unlock:
     wlExternalApiUnlock();
+fail:
     wlEglSetError(data, err);
     return EGL_NO_STREAM_KHR;
 }

Submitted upstream as https://github.com/NVIDIA/egl-wayland/pull/194

Appendix

Crude python script used to make GDB dump where and who took and released the mutex.

Mashed up GDB python plugin

#!/usr/bin/env python3

import json
import logging
from collections import defaultdict
from dataclasses import dataclass

import gdb

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass
class Trace:
kind: Literal["lock", "unlock"] thread_id: int
stacktrace: List[str]

# Mutex address -> trace info
_trace_db: Dict[int, List[str]] = defaultdict(list)

_breakpoints: List[gdb.Breakpoint] = []

class MutexLockWatchpoint(gdb.Breakpoint):
def __init__(self, trace_db: Dict[int, List[str]], track_lock: bool):
if track_lock:
super().__init__("wlExternalApiLock", gdb.BP_BREAKPOINT)
else:
super().__init__("wlExternalApiUnlock", gdb.BP_BREAKPOINT)
self.trace_db = trace_db
self.track_lock = track_lock

def stop(self) -> bool:
stacktrace = gdb.execute("bt", to_string=True)

mutex_address = int(gdb.parse_and_eval("&wlMutex"))

self.trace_db[mutex_address].append(
Trace(kind="lock" if self.track_lock else "unlock", thread_id=gdb.selected_thread().ptid[0],
stacktrace=stacktrace.splitlines()))
logger.info(
f"Mutex {mutex_address:016x} {'locked' if self.track_lock else 'unlocked'} by thread {gdb.selected_thread().ptid[0]}")

def start_watch():
global _breakpoints
global _trace_db
if len(_breakpoints) == 0:
_breakpoints.append(MutexLockWatchpoint(_trace_db, True))
_breakpoints.append(MutexLockWatchpoint(_trace_db, False))
else:
logger.warning("Watchpoints already set up. Skipping.")

def end_watch():
global _breakpoints
for bp in _breakpoints:
bp.delete()
_breakpoints = []

def dump_traces(out_file: str = None):
global _trace_db

mapped = {k: [{"kind": t.kind, "thread_id": t.thread_id, "stacktrace": t.stacktrace} for t in v] for k, v in _trace_db.items()}

with open(out_file, "w") as f:
json.dump(_trace_db, f, indent=2)
logger.info(f"Traces dumped to {out_file}")

class MutexWatchCmd(gdb.Command):
def __init__(self):
super().__init__("mutex_watch", gdb.COMMAND_DATA)

def invoke(self, arg, from_tty):
argv = gdb.string_to_argv(arg)
if len(argv) == 0:
logger.info("Usage: mutex_watch ")
return

if argv[0] == "start":
start_watch()
return

if argv[0] == "end":
end_watch()
return

if argv[0] == "dump":
if len(argv) < 2: logger.info("Where to dump???") return dump_traces(argv[1]) return if __name__ == "__main__": MutexWatchCmd()

Photo Roll from Hokkaido

Former Ministry From Edo Era in Goryokaku
Lat N 41 47.813 Long E 140 45.430
Fireworks “Hanabi” at Toya Lake
Lat N 42 33.970 Long E 140 49.228
f/16 30” ISO 640
Sapporo Subway O-dori Station, Exit to West-3 Intersection
Lat N 43 03.648 Long 141 21.118
Hakodate Bay Night View from Hakodate Mountain Observatory
Lat N 41 45.562 Long E 140 42.278
f/10 10” ISO 100

A Note on SIT Tunnel at Home

This blog article records the progress of setting up IPv6 tunnel to Hurricane Electric at home where my ISP has no plan in sight to provide prefix-delegated IPv6 access over a PPPoE.

Shorthands and Assumptions in This Note

  • eth0 connects to the Internet via IPv4. This note shall also apply to encapsulated interfaces, e.g. vlan15@eth0, lte0 or pppoe0.
  • eth1 connects to local LAN. Similar as above, the process shall be the same when LAN side is a VLAN or bridge (or both).
  • tun0 denotes the sit tunnel interface created in this step.
  • A line beginning with # denotes comments in the configuration notes.

Setting up the Tunnel Interface on ER-X

If registered correctly on TunnelBroker, it should provide the following information:

  • Logical address at local endpoint, e.g. 2001:444:111:222::2/64
  • Logical address at remote endpoint, e.g. 2001:444:111:222::1/64
  • IPv4 address at remote endpoint where encapsulated traffic is sent, e.g. 66.220.18.42, the HE tunneling endpoint in Paris.
  • A routable prefix for client side delegation, e.g. 2001:444:112:222::/64. This is usually different from the v6 addresses for the endpoint, and HE will show segments of the prefix in bold.

Now fill ER-X configuration nodes with corresponding information and default routing for IPv6:

interfaces:
    tunnel:
        tun0:
            address: [Fill logical v6 address in CIDR at endpoint]
            description: [Give a name to this tun]
            encapsulation: sit
            local-ip: [Fill in IPv4 address at eth0]
            remote-ip: [Fill in IPv4 address at tunneling endpoint]
protocols:
    static:
        interface-route6:
            ::/0:
                next-hop-interface: tun0
            # This creates a default IPv6 routing table entry that
            # routes all non-link-local address to the tunnel.

At this point, one should be able to ping any IPv6 address from the ER-X. If this is working, continue to instruct the LAN interface to delegate the prefix

interface:
    <path-to-interface-config-node>:
        ipv6:
            dup-addr-detect-transmits: 1
            # Stateless SLAAC configuration might produce identical
            # IP addresses. This allow the network to detect whether
            # a stateless address already exists.
            address:
                autoconf
                # Set autoconf to allow stateless delegation by SLAAC
            router-advert:
                prefix:
                    [Fill routable delegated prefix here]:
                        autonomous-flag: true
                        # Instructs computers on this network to auto
                        # discover DNS servers
                        on-link-flag: true
                        # Indicates that this prefix exists on the
                        # same Ethernet link, i.e. these addresses
                        # does not require routing

IPv6 enabled devices shall now receive globally unique IPv6 address assigned via SLAAC and prefix delegation.

Subsequent Steps

  • Confirm IPv6 assignment on LAN devices
$ ip addr
<------ MORE INTERFACES REDACTED ------>
2: eno1:  mtu 1480 qdisc fq_codel state UP group default qlen 1000
     link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
     inet 192.168.5.4/24 brd 192.168.5.255 scope global dynamic noprefixroute eno1
        valid_lft 80444sec preferred_lft 80444sec
     inet6 2001:470:d:XXXX:XXXX:XXXX:XXXX:dfd3/64 scope global dynamic noprefixroute 
        valid_lft 2591976sec preferred_lft 86376sec
     inet6 fe80::be40:XXXX:XXXX:XXXX/64 scope link noprefixroute 
        valid_lft forever preferred_lft forever
<------ MORE INTERFACES REDACTED ------>
  • Trace IPv6 connections to an IPv6 enabled website
$ traceroute -6 ac.cth451.me -n
 traceroute to ac.cth451.me (2606:4700:30::681c:1b16), 30 hops max, 80 byte packets
  1  2001:470:d:XXXX:XXXX:XXXX:XXXX:XXXX  0.415 ms  0.533 ms  0.624 ms
  2  2001:470:c:XXXX::1  185.353 ms *  203.802 ms
  3  2001:470:0:9d::1  178.603 ms  167.001 ms  189.255 ms
  4  2001:504:0:3:0:1:3335:1  196.520 ms  179.737 ms  196.117 ms
  5  2400:cb00:12:1024::6ca2:d61d  185.036 ms 2400:cb00:12:1024::6ca2:d614  175.573 ms 2400:cb00:12:1024::6ca2:d608  185.263 ms
  • It is advisable to setup network wide firewall on the router, as addresses can be reached by any other IPv6 connected devices from the Internet.

Further Notes

  • The sit tunnel shall also work if setup correctly on any other router or even a personal computer with public IPv4 address. I am unable to replicate the settings on a Linux router via raw commands as I do not own a linux machine with public IPv4 address.
  • I am not sure if the method would work if the local endpoint is behind NAT. This scenario will be experimented on after I return to campus.

“Certified” Androids?

The story begins with the moment I bought Cytus II on the very first day of its release from google play, expecting some real music gaming on my Surface Pro 3 running homebrew Android 7.1 (yes, that’s totally possible and it runs suprisingly fine with all hardware buttons and touchscreen working). However the game quits immediately and no logs are shown through the adb interface. I contacted Rayark for support and got reply like this:

Cytus II might only be compatible for native Android devices at the moment. Please also check if you have installed Xposed, firewalls, block ads, or any rooting software. If yes, these may effect the performance of the game. We’d like to suggest you to remove these software to ensure the game runs smoothly and properly.

(Probably) 3 weeks after the game’s release, I ended up playing Cytus II on my crammed 5′ mobile phone, not by choice though. Someone asked me, why I can’t buy a regular Android tablet or an iPad to do the same job. Well that could be an option if those people would donate in any means for a new one, and I really don’t think utilizing an existing hardware piece could cause any troubles for commercial devs and companies (Google: really?).

After some searches I reached conclusion that Cytus II has integrated an “compliance check” called SafetyNet and the underlying Compatibility Test Suite (CTS), a framework introduced by Google to verify if any android device falls into the category of “compatible”. Since Surface Pro 3 has never got an official Android release (it’s a Microsoft thing, of course) and the base android x86 is shipped with root and development mode on, there is probably no way that any CTS tests on Surface Pro 3 would pass any time sooner.

Just earlier this day, I saw the news that Google is attmpting to block Gapps from running on “uncertified” devices, where modding the android device or unlocking bootloader would void the “certified” status. There’s even a webpage letting people to “register” their Androids with their device identifier which is absolutely not working after I attempted to register my Surface only getting an unknown error.

I would not blame Rayark for their attempt to place a layer of piracy protection on such a nice game while hurting the ones who modded their devices properly exactly to play these games. My question is: If such a lockdown is so important that this system had been deployed to thousands of android software by now, why make android an open standard? Why not switch to the Apple production mode if a centralized control force seems so vital to the whole android community? Such blockage wouldn’t be easily tolerated if the so called “register uncertified devide page” is just a lie, and I believe there will be a solution to circumvent such unreasonable restrictions eventually. Before that, the vast population of “uncertified” androids and modders wouldn’t be so comfortable and I might really need to ask myself: Why I should spend 3 months porting an open source OS to a new device just to find nothing should works by design.

The Hot City?

When I told someone Chengdu is a hot city, I meant both food and the city itself. Hopefully, I finally remembered that I forgot procrastinated to post my photos I took when I travelled to Chengdu and stayed for a few days this winter. Still, these pictures are taken from my old Canon 50D DSLR and the mediocre lens.

Continue reading The Hot City?

The season of white (photo) album

It’s -30 degree celcius in Madison. It’s snowing. It’s the season of white album again.

白い雪が街に——
優しく積もるように
アルバムの空白を全部——
埋めてしまおう——

Camera: Canon EOS 50D (a really old DSLR)
Lens: Canon EF-S 17-85mm f4-5.6 IS USM (yes, the very mediocre default)
Aperture: f/5.6
Exposure: 1/500
Location: In my room [precise location redacted]

[Errata] Windows upgrade to 1703 broke grub

What happened?

After windows upgrading itself to release 1703 aka. the creator update, Grub bootloader can no longer start and says error: unknown filesystem and dropped in to grub rescue shell. Windows boots up fine from firmware memu.

TL; DR

Manually doing hexediting just taught me another valuable lesson: @Windows has the worst upgrade handling.

— @cth451 July 28, 2017

  1. Prepare a USB linux live environment.
  2. Find the correct partition number, it might have been changed during upgrade.
  3. Look for grubx64.efi in your EFI system partition and use a hex editor to open it
  4. Search for /boot/grub and notice the (,gptN) where N is your original partition number for linux OS.
  5. Change N to the new parition number, save the file and reboot.

Analysis

Before upgrade, my partition scheme was:

  • sda1 fat32: EFI system parition
  • sda2 ext4: Archlinux root filesystem
  • sda4 ntfs: Windows drive C
  • sda3 ext4: AOSC OS root filesystem

Partition numbers were not exactly in physical order of data, for I shrinked Archlinux to make room for Windows installation. However after upgrade, it became:

  • sda1 fat32: EFI system parition
  • sda2 ext4: Archlinux root filesystem
  • sda3: Windows drive C
  • sda4: Windows recovery environment
  • sda5: AOSC OS root filesystem

So what did windows do down there? It shrinked drive C and made a little recovery partition, then re-sorted the partition numbers. However grub bootloader looks for partitions by partition number. It tried parition 3 (which was a valid linux filesystem), but it’s an ntfs now.

This partition number is hardcoded into the bootloader efi image, so we need to manually hexedit it.

Lake superior and sunset

Lake Superior – Sugarloaf Cove

Position: 47°29’11″N 90°58’59″W

Cobblestone underwater. Taken on sugarloaf cove beach with waterproof Xperia Z5.
Cobblestone underwater on sugarloaf cove beach with waterproof Xperia Z5.

I took this photo with the waterproof Xperia upside down with camera underwater. Not exactly what I expected but still amazing enough to see colorful cobblestone underwater.

Colorful cobblestones on the beach of Lake Superior
Colorful cobblestones on the beach of Lake Superior

Yet another regular collection of colorful stone beach.

Lake Mille Lacs

Position: 46°10’52″N 93°43’42″W

Lake Mille Lacs, partially frozen. Taken on a trail 2m from the shore.
Lake Mille Lacs, partially frozen. Taken on a trail 2m from the shore.

This one is pretty off-road on the side of a trail beyond Indians point. Cold, but beautiful.

Minnesota Route 1

Sunset on MN-1 westbound
Sunset on MN-1 westbound

Unfortunately I lost Geo-tag in this photo… So I don’t know the precise location on the road.

Goodbye, chunky. Hi, Blender! (with the Interior Of Dat Triangle)

This would be one of my last frames of Minecraft renderworks with chunky. I will be switching to blender, an open source modeling software that supports GPU ray tracing with CUDA and allows tweaks to model before performing actual (now night-long) rendering.

Chunky is still pretty straightforward to newbie CG makers, written in java that virtually runs everywhere, but it does not support GPU rendering, and java multi-threading overhead made it even worse.

Blender, on the other hand, needs a few hours to setup but provides far more precise control over elements, vertices, edges and fancy stuff. Cropping down map portions out of the camera view, customize torch light color, removing specific blocks, render entities with actual motion…

So… Goodbyeさようなら, chunky. Hi, Blender!

I might still recommend chunky for those who want to have a sip of minecraft rendering, but for more complex terrain and huge creations, use blender.

Here’s the downscaled rendered frame, but you can still find full resolution on deviant art.


Rendering Specs:


Rendering time: 1.57 days
Threads: 3
Original dimensions: 4000x4000
Downscaled dimensions for uploading: 2000x2000