How to Install Sunshine on macOS: A Step-by-Step Guide

Setting up Sunshine on macOS requires some preliminary steps to ensure all dependencies and configurations are in place. In this blog post, we will walk through the entire installation process, step by step, so you can easily get Sunshine running on your Mac system.

Install MacPorts

The first thing we need to do is install MacPorts, a package management system for macOS. Follow the instructions on the official MacPorts installation page.

Add Sunshine to MacPorts

Before we can install Sunshine, we need to add it to our MacPorts source list.

Update MacPorts Sources:

echo "file:///Users/$USER/ports" | sudo tee -a /opt/local/etc/macports/sources.conf

Download Portfile for Sunshine

First, navigate to the folder where you'll place the Portfile. Create it if it doesn't exist

mkdir -p ~/ports/multimedia/sunshine

Download the Portfile using curl

curl -L -o ~/ports/multimedia/sunshine/Portfile

Update Port Index and Install Sunshine

Navigate to the ports directory and update the port index:

cd ~/ports
sudo portindex

Finally, install Sunshine:

sudo port install sunshine

Run Sunshine

Launch Sunshine by running the following command in the Terminal:


The first time you start Sunshine, you will be asked to grant access to screen recording and your microphone.

Sunshine will start a web server, which you can access at https://localhost:47990. Remember to accept the self-signed certificate warning.

Configure macOS Audio

Install BlackHole Audio Sink

BlackHole is an audio sink that will help us in redirecting audio. Open your Terminal and run:

sudo port install BlackHole

For audio, make sure you set "BlackHole 2ch" as your input device in the Sunshine configuration. Even though "BlackHole 2ch" appears as the placeholder text, you'll have to manually type it into the field to set it.

And that's it! You have successfully installed and configured Sunshine on your macOS system. Enjoy streaming your macOS desktop and audio to other devices!

CI/CD macOS Application Bundling .app Artifacts

CI/CD macOS Application Bundling .app Artifacts

1. macOS Application Bundle Structure

1.1 The .app Bundle Format

In macOS, applications are distributed as bundles, a directory structure that contains executable code and related resources. Here's the typical structure of a .app bundle:
  • Info.plist: Metadata and configuration information.
  • MacOS/: Main executable file.
  • Resources/: Stores resource files, such as images and localized content.
1.2 Understanding the Info.plist File

A .app bundle is really just a directory. The Info.plist file is critical to the .app bundle as it is what defines how to use this folder as a launcher instead of a folder. It contains key-value pairs that macOS uses to index the application. Key elements include:

  • CFBundleExecutable: Specifies the main executable file.
  • CFBundleIconFile: Defines the icon file.

Here's an example snippet:


2. ICNS Conversion

macOS uses the ICNS file format for icons. A convenient conversion tool is png-to-icns-right-click-converter. This tool allows for seamless conversion from PNG to ICNS, suitable for inclusion in the application bundle's Resources directory.

3. CI/CD Integration

3.1 YAML Configuration for Packaging macOS Application

Continuous Integration and Continuous Deployment (CI/CD) streamline the development workflow. Below is a YAML script that demonstrates how to package a macOS application binary:

    stage: package_stage
        - build_stage
        - INFO_PLIST="$"
        - echo "Packing self-contained build for macOS x64..."
        - mkdir -p $
        - mkdir -p $
        - cp Resources/Icons/icon.icns $
        - cp $BUILD_DIR/$BINARY_NAME $
        - chmod +x $$BINARY_NAME
        - echo '<?xml version="1.0" encoding="UTF-8"?>' > $INFO_PLIST
        - echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">' >> $INFO_PLIST
        - echo '<plist version="1.0">' >> $INFO_PLIST
        - echo '<dict>' >> $INFO_PLIST
        - echo '    <key>CFBundleExecutable</key>' >> $INFO_PLIST
        - echo "    <string>$BINARY_NAME</string>" >> $INFO_PLIST
        - echo '    <key>CFBundleIconFile</key>' >> $INFO_PLIST
        - echo '    <string>icon.icns</string>' >> $INFO_PLIST
        - echo '</dict>' >> $INFO_PLIST
        - echo '</plist>' >> $INFO_PLIST
        name: "macOS x64 (self-contained)"
            - $BUILD_DIR/$
        expire_in: 1 week


macOS applications rely on a bundle structure, where an application is organized as a directory with the .app extension. This structure includes executable code, resources, and metadata, all defined within specific subdirectories and files such as .plist and ICNS icon files. Understanding these aspects allows developers to create CI/CD bundling stages for any macOS app.

Correcting Perspective Projections in 3D Graphics: 2009Scape Ground Items

One of the most critical operations in computer graphics is the projection of a 3D world onto a 2D plane. This operation allows 3D objects to be displayed on conventional 2D screens, enabling us to play 3D video games or run simulations. This transformation is achieved through perspective projections. However, issues can arise when these transformations don't account for different screen sizes or aspect ratios, resulting in skewed and distorted images. In this article, we will delve into the process of correcting these perspective distortions.

Perspective Projections: A Primer

Perspective projection is a type of projection in which the 3D coordinates of an object are transformed into 2D coordinates. It creates an illusion of depth and distance. The further away an object is from the viewer, the smaller it appears on the screen.

However, the proportions of objects can sometimes be distorted due to different screen sizes and aspect ratios. The goal of our investigation was to correct this distortion.

A typical function for creating a perspective projection looks like this:

// Method to set the projection matrix used in OpenGL rendering
private static void setProjectionMatrix(
    float left,      // arg0
    float right,     // arg1
    float bottom,    // arg2
    float top,       // arg3
    float nearClip,  // arg4
    float farClip    // arg5
) {
    float nearClipDouble = nearClip * 2.0F;

    // Set the elements of the projection matrix
    matrix[0] = nearClipDouble / (right - left);
    matrix[1] = 0.0F;
    matrix[2] = 0.0F;
    matrix[3] = 0.0F;
    matrix[4] = 0.0F;
    matrix[5] = nearClipDouble / (top - bottom);
    matrix[6] = 0.0F;
    matrix[7] = 0.0F;
    matrix[8] = (right + left) / (right - left);
    matrix[9] = (top + bottom) / (top - bottom);
    matrix[10] = -(farClip + nearClip) / (farClip - nearClip);
    matrix[11] = -1.0F;
    matrix[12] = 0.0F;
    matrix[13] = 0.0F;
    matrix[14] = -(nearClipDouble * farClip) / (farClip - nearClip);
    matrix[15] = 0.0F;

    // Load the created matrix into OpenGL
    gl.glLoadMatrixf(matrix, 0);


Incorporating Field of View (FOV)

Field of view (FOV) plays a critical role in 3D projections. It represents the extent of the observable world at any given moment. FOV essentially determines how zoomed in or zoomed out the final image appears. Horizontal FOV (hFOV) and Vertical FOV (vFOV) represent the FOV in the horizontal and vertical directions, respectively.

To correct the perspective projection, we started by extracting the hFOV and vFOV from the projection matrix. The projection matrix, in computer graphics, is a matrix that transforms the coordinates in the view space to the clip space. We reverse-engineered this matrix and replaced hardcoded FOV values with the extracted hFOV and vFOV. Here is a simplified example of such a function:

The horizontal and vertical FOVs (field of view) are calculated based on the values of the left, right, bottom, and top clipping planes and the near clipping distance. They represent the extent of the observable world that can be seen from the perspective of the camera at any given moment. The calculations use the formula 2 atan((right - left) / (2 nearClip)) and 2 atan((top - bottom) / (2 nearClip)) respectively, which are derived from simple trigonometry.

// Method to set the projection matrix used in OpenGL rendering
private static void setProjectionMatrix(
    float left,      // arg0
    float right,     // arg1
    float bottom,    // arg2
    float top,       // arg3
    float nearClip,  // arg4
    float farClip    // arg5
) {
    float nearClipDouble = nearClip * 2.0F;


    // Calculate the horizontal and vertical field of view
    double hFOV = 2 * Math.atan((right - left) / nearClipDouble);
    double vFOV = 2 * Math.atan((top - bottom) / nearClipDouble);

    // Convert to degrees
    hFOV = Math.toDegrees(hFOV);
    vFOV = Math.toDegrees(vFOV);

    // Load the created matrix into OpenGL
    gl.glLoadMatrixf(matrix, 0);

Implementing the Solution

With the hFOV and vFOV now available, we could adjust the projection of our 3D world onto the 2D screen accordingly.

Our application involves a function that transforms 3D world coordinates (entityX, entityY, entityZ) into 2D screen coordinates. The challenge was to adjust the entity's screen position (spriteDrawX, spriteDrawY) based on the hFOV and vFOV. Here's how the function looked like:

 * Calculates the 2D screen position for a position in the SceneGraph.
 * @param entityX The x-coordinate of the entity in the scene graph.
 * @param entityZ The z-coordinate of the entity in the scene graph.
 * @param yOffset The vertical displacement for positioning the entity.
 * @return An array containing the calculated screen coordinates [x, y] or [-1, -1] if entity is not visible.
public static int[] CalculateSceneGraphScreenPosition(int entityX, int entityZ, int yOffset) {
    final int HALF_FIXED_WIDTH = 256;
    final int HALF_FIXED_HEIGHT = 167;

    int elevation = SceneGraph.getTileHeight(plane, entityX, entityZ) - yOffset;
    entityX -= SceneGraph.cameraX;
    elevation -= SceneGraph.cameraY;
    entityZ -= SceneGraph.cameraZ;

    int sinPitch = MathUtils.sin[Camera.cameraPitch];
    int cosPitch = MathUtils.cos[Camera.cameraPitch];
    int sinYaw = MathUtils.sin[Camera.cameraYaw];
    int cosYaw = MathUtils.cos[Camera.cameraYaw];

    int rotatedX = (entityZ * sinYaw + entityX * cosYaw) >> 16;
    entityZ = (entityZ * cosYaw - entityX * sinYaw) >> 16;
    entityX = rotatedX;

    int rotatedY = (elevation * cosPitch - entityZ * sinPitch) >> 16;
    entityZ = (elevation * sinPitch + entityZ * cosPitch) >> 16;
    elevation = rotatedY;

    int[] screenPos = new int[2]; // X,Y

    if (entityZ >= 50) {
        if(GetWindowMode() == WindowMode.FIXED) {
            screenPos[0] = HALF_FIXED_WIDTH + ((entityX << 9) / entityZ);
            screenPos[1] = HALF_FIXED_HEIGHT + ((elevation << 9) / entityZ);
        } else {
            Dimension canvas = GetWindowDimensions();
            double newViewDistH = (canvas.width / 2) / Math.tan(Math.toRadians(GlRenderer.hFOV) / 2);
            double newViewDistV = (canvas.height / 2) / Math.tan(Math.toRadians(GlRenderer.vFOV) / 2);
            screenPos[0] = canvas.width / 2 + (int)((entityX * newViewDistH) / entityZ);
            screenPos[1] = canvas.height / 2 + (int)((elevation * newViewDistV) / entityZ);
    } else {
        screenPos[0] = -1;
        screenPos[1] = -1;
    return screenPos;

We used the tangent function from trigonometry to calculate the new viewing distance based on hFOV and vFOV, and used it to calculate the new screen positions.

Practical Application: 2009Scape Plugin

This theory becomes practical in the realm of game development. For example, in a plugin developed for the game 2009Scape, this corrected perspective projection is used to draw text above items onscreen. This plugin is a quality of life add-on that helps players determine which item drops are valuable and which should be ignored. Accurate world-to-screen projections are crucial in ensuring the text appears at the correct on-screen location, regardless of the player's perspective or window size.

Wrapping Up

This journey into perspective projections and field of view showcases the importance of understanding fundamental computer graphics concepts. By correctly implementing these concepts, we can create a more enjoyable and realistic gaming experience, improve our computational efficiency, and broaden our understanding of 3D graphics in general. It's a testament to the power of computer graphics when well understood and well applied.

Stay tuned for more deep dives into the world of computer graphics and game development. Until next time!

Gaming in Thailand as a Foreigner

Hey gamers! It's world famous video gamer downthecrop! Back from my Thailand adventure and sharing the wisdom my globe trotting has afforded me!

General Internet Speed

This is going to depend mostly on your accommodation. I had 30/10 internet through Coax in my building which I'd estimate finished construction in the mid 2000's. You can find fiber internet in Bangkok but I'd assume most places are still Coax or DSL instead of fiber. Most internet is routed through Chang Mai anyway from the traceroutes I did while I was there.

Gaming VPN's

While there is a Thai server for League of Legends on Garena a problem you will experience in Thailand is that there just isn't a large enough population to support some games. League of Legends for example I was getting queue times exceeding 15 minutes at peak times (Noon to 11:00PM) in ranked which is extremely long compared to NA/EU/KR. To remedy this you are going to need to select the closest server with an active playerbase for your favorite games! I played mostly on Vietnam for League of Legends. Although you can simply change the region of the Garena Launcher and download any game you want there is IP based region lock for some games. League of Legends falls into these IP locked games. Because of this we are going to need to use a VPN.

Generally VPN's aren't very good for gaming. Due to oversold traffic and a lack of fast localized routers to handle your traffic you are going to be experiencing higher latency playing through a VPN than without one. There are a few VPN providers that SEA use to get their game on!

  • Mudfish
  • VietPN
  • PingBooster

Note: Exitlag and WTFast have very few servers in SEA so I wouldn't recommended them as a first choice for this use case but they will work.

For me I used VietPN and it cost me $5 a month which was a great price. Fast servers to Vietnam which is region locked for League of Legends. I would have preferred to play to China (Ionia probably the worst choice for server because it's so far away but there are many regions in China which are closer and would still have more players than TH or SG server which were the two best choices for ping.

Anyway I'm not spell checking this I just wanted to document for anyone else that is thinking of gaming in Thailand!

Lytical: OP.GG for Garena and China (SEA) – League of Legends Statistics and Profile Search

Lytical is an open source League of Legends statistics and profile analytics tool for all regions including Garena & WeGame

Summoner Search and Profile Pages

Lytical offers OP.GG style summoner profiles which can be searched for anyone on your server. Our app offers ranked statistics and champion specific performance data for all players for Garena and WeGame. Lytical is an OP.GG alternative for all Garena regions and all Chinese regions and is an all in one replacement for websites like U.GG, Blitz.GG and Porofessor of course OP.GG.

Think of Lytical as OP.GG for Garena.

Match History & Lobby Multisearch

Lytical provides fast OP.GG style match history for any player. Kills, CS per Minute, Vision Score, and Objectives. Analyze your contribution to your own games or search high ranked players to increase your impact and win more games!

Multisearch will automatically scout your teammates in champion select and your opponents when in game. Profile lookup tools are common on NA/EU/KR servers to help avoid poor quality games. Lytical is the tool for the job to quickly understand your teammates and opponents strengths and weaknesses.

Works on All Regions

Lytical utilizes builtin API's inside of the League of Legends client. This allows our application to function on all regions, including all Garena and WeGame regions.

Supported Servers/Regions

  • Garena
  • The Philippines (PH)
  • Taiwan, Hong Kong, and Macau (TW/HK)
  • Vietnam (VN)
  • Thailand (TH)
  • Singapore, Malaysia, and Indonesia (SG/MY/ID)
  • QQ/WeGame/Tencent (CN)
  • Riot Operated (NA/EU/OCE/RU/TR/BR/LAN/KR/JP)

Open Source and Ad Free

The Lytical app is forever free and open source. It was created as an All-In-One replacement for regions that don't have access to statistics sites like OP.GG or

Built Using Vue.JS + Electron Lytical is fast and memory efficient. Any PC can run Lytical in the background with no performance impact in game for League of Legends.

This application is released under the Open Source GPLv2 license. You are welcome to contribute to this project in any way you can.