Scenic.Cache.Static.Texture (Scenic v0.10.3) View Source

In memory cache for static static Image assets.

In graphics-speak, an image that is being drawn to the screen is a "Texture". It doesn't matter how the image was stored on the disk (jpg, png, etc.), they all end up as textures presented to the graphics card.

Assets such as textures tend to be relatively large compared to other data. These assets are often used across multiple scenes and may need to be shared with multiple drivers.

Goals

Given this situation, the Cache module has multiple goals.

  • Reuse - assets used by multiple scenes should only be stored in memory once
  • Load Time- loading cost should only be paid once
  • Copy time - assets are stored in ETS, so they don't need to be copied as they are used
  • Pub/Sub - Consumers of static assets (drivers...) should be notified when an asset is loaded or changed. They should not poll the system.
  • Security - Base assets can become an attack vector. Helper modules are provided to assist in verifying these files.

Scope

When a texture term is loaded into the cache, it is assigned a scope. The scope is used to determine how long to hold the asset in memory before it is unloaded. Scope is either the atom :global, or a pid.

The typical flow is that a scene will load a font into the cache. A scope is automatically defined that tracks the asset against the pid of the scene that loaded it. When the scene is closed, the scope becomes empty and the asset is unloaded.

If, while that scene is loaded, another scene (or any process...) attempts to load the same asset into the cache, a second scope is added and the duplicate load is skipped. When the first scene closes, the asset stays in memory as long as the second scope remains valid.

When a scene closes, it's scope stays valid for a short time in order to give the next scene a chance to load its assets (or claim a scope) and possibly re-use the already loaded assets.

This is also useful in the event of a scene crashing and being restarted. The delay in unloading the scope means that the replacement scene will use already loaded assets instead of loading the same files again for no real benefit.

When you load assets you can alternately provide your own scope instead of taking the default, which is your processes pid. If you provide :global, then the asset will stay in memory until you explicitly release it.

Hashes

At its simplest, accessing the cache is a key-value store. This cache is meant to be static in nature, so the key is be a hash of the data.

Why? Read below...

Security

A lesson learned the hard way is that static assets (fonts, images, etc.) that your app loads out of storage can easily become attack vectors.

These formats are complicated! There is no guarantee (on any system) that a malformed asset will not cause an error in the C code that interprets it. Again - these are complicated and the renderer needs to be fast...

The solution is to compute a SHA hash of these files during build-time of your and to store the result in your applications code itself. Then during run time, you compare then pre-computed hash against the run-time of the asset being loaded.

Please take advantage of the helper modules Cache.Support.File and Cache.Support.Hash to build the hashes.

These scheme is much stronger when the application code itself is also signed and verified, but that is an exercise for the packaging tools.

Full Example:

defmodule MyApp.MyScene do
  use Scenic.Scene
  import Scenic.Primitives

  # build the path to the static asset file (compile time)
  @image_path :code.priv_dir(:my_app) |> Path.join("/static/images/my_image.jpg")
  @image_hash Scenic.Cache.Hash.file!( @image_path, :sha )

  @graph Graph.build()
  |> rect( {10, 20}, fill: {:image, @image_hash})

  def init( _, _ ) do
    # load the asset into the cache (run time)
    Scenic.Cache.Static.Texture.load(@image_path, @image_hash)

    {:ok, :some_state, push: @graph}
  end
end

Pub/Sub

Drivers (or any process...) listen to the font Cache via a simple pub/sub api.

Because the graph, may be computed during compile time and pushed at some other time than the assets are loaded, the drivers need to know when the assets become available.

Whenever any asset is loaded into the cache, messages are sent to any subscribing processes along with the affected keys. This allows them to react in a loosely-coupled way to how the assets are managed in your scene.

Link to this section Summary

Functions

Add a scope to an existing texture in the cache.

Tests if a key is claimed by the given scope.

Retrieve a texture from the cache and wrap it in an {:ok, _} tuple.

Retrieve a texture from the Cache.

Retrieve a texture from the Cache and raise an error if it doesn't exist.

Returns a list of keys claimed by the given scope.

Tests if a key is claimed by any scope.

Insert a new texture into the Cache.

Release a scope claim on an texture.

Get the current status of a texture in the cache.

Subscribe the calling process to cache messages.

Unsubscribe the calling process from cache messages.

Link to this section Functions

Link to this function

claim(hash, scope \\ nil)

View Source

Specs

claim(
  hash :: Scenic.Cache.Base.hash(),
  scope :: :global | nil | GenServer.server()
) :: term()

Add a scope to an existing texture in the cache.

Claiming an asset in the cache adds a lifetime scope to it. This is essentially a refcount that is bound to a pid.

Returns true if the item is loaded and the scope is added. Returns false if the asset is not loaded into the cache.

Link to this function

claimed?(hash, scope \\ nil)

View Source

Specs

claimed?(
  hash :: Scenic.Cache.Base.hash(),
  scope :: :global | nil | GenServer.server()
) :: true | false

Tests if a key is claimed by the given scope.

Specs

fetch(hash :: Scenic.Cache.Base.hash()) :: term() | {:error, :not_found}

Retrieve a texture from the cache and wrap it in an {:ok, _} tuple.

This function ideal if you need to pattern match on the result.

Examples

iex> Elixir.Scenic.Cache.Static.Texture.fetch("missing_hash")
...> {:error, :not_found}

iex> Elixir.Scenic.Cache.Static.Texture.fetch("valid_hash")
...> {:ok, :test_data}
Link to this function

get(hash, default \\ nil)

View Source

Specs

get(hash :: Scenic.Cache.Base.hash(), default :: term()) :: term() | nil

Retrieve a texture from the Cache.

If there is no item in the Cache that corresponds to the hash the function will return either nil or the supplied default value

Examples

iex> Elixir.Scenic.Cache.Static.Texture.get("missing_hash")
nil

...> Elixir.Scenic.Cache.Static.Texture.fetch("valid_hash")
{:ok, :test_data}

Specs

get!(hash :: Scenic.Cache.Base.hash()) :: term()

Retrieve a texture from the Cache and raise an error if it doesn't exist.

If there is no item in the Cache that corresponds to the hash the function will raise an error.

Specs

keys(scope :: :global | nil | GenServer.server()) :: list()

Returns a list of keys claimed by the given scope.

Link to this function

load(path, hash, opts \\ [])

View Source

Callback implementation for Scenic.Cache.Base.load/3.

Link to this function

load!(path, hash, opts \\ [])

View Source

Callback implementation for Scenic.Cache.Base.load!/3.

Specs

member?(hash :: Scenic.Cache.Base.hash()) :: true | false

Tests if a key is claimed by any scope.

Link to this function

put_new(hash, data, scope \\ nil)

View Source

Specs

put_new(
  hash :: Scenic.Cache.Base.hash(),
  data :: term(),
  scope :: :global | nil | GenServer.server()
) :: term()

Insert a new texture into the Cache.

If the texture is already in the cache, put_new does nothing and just returns {:ok, hash}

Parameters:

  • hash - term to use as the retrieval key. Typically a hash of the data itself. It will be required to be a hash of the data in the future.
  • data - term to use as the stored data
  • scope - Optional scope to track the lifetime of this asset against. Can be :global but is usually nil, which defaults to the pid of the calling process.

Returns: {:ok, hash}

Link to this function

release(hash, opts \\ [])

View Source

Specs

release(hash :: Scenic.Cache.Base.hash(), opts :: list()) :: :ok

Release a scope claim on an texture.

Usually the scope is released automatically when a process shuts down. However if you want to manually clean up, or unload an asset with the :global scope, then you should use release.

Parameters:

  • key - the key to release.
  • options - options list

Options:

  • scope - set to :global to release the global scope.
  • delay - add a delay of n milliseconds before releasing. This allows starting processes a chance to claim a scope before it is unloaded.
Link to this function

status(hash, scope \\ nil)

View Source

Specs

status(
  hash :: Scenic.Cache.Base.hash(),
  scope :: :global | nil | GenServer.server()
) :: :ok

Get the current status of a texture in the cache.

This is used to test if the current process has claimed a scope on an asset.

Link to this function

subscribe(hash, sub_type \\ :all)

View Source

Specs

subscribe(
  hash :: Scenic.Cache.Base.hash() | :all,
  sub_type :: Scenic.Cache.Base.sub_types()
) :: :ok

Subscribe the calling process to cache messages.

Parameters

  • hash - The hash key of the asset you want to listen to messages about. Pass in :all for messages about all keys
  • sub_type - Pass in the type of messages you want to unsubscribe from.
    • :put - sent when assets are put into the cache
    • :delete - sent when assets are fully unloaded from the cache
    • :claim - sent when a scope is claimed
    • :release - sent when a scope is released
    • :all - all of the above message types
Link to this function

unsubscribe(hash, sub_type \\ :all)

View Source

Specs

unsubscribe(
  hash :: Scenic.Cache.Base.hash() | :all,
  sub_type :: Scenic.Cache.Base.sub_types()
) :: :ok

Unsubscribe the calling process from cache messages.

Parameters

  • hash - The hash key of the asset you want to listen to messages about. Pass in :all for messages about all keys
  • sub_type - Pass in the type of messages you want to unsubscribe from.
    • :put - sent when assets are put into the cache
    • :delete - sent when assets are fully unloaded from the cache
    • :claim - sent when a scope is claimed
    • :release - sent when a scope is released
    • :all - all of the above message types