diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..125ee7a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.xcuserdatad diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7105990 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Jacob Bandes-Storch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Metalbrot.playground/Contents.swift b/Metalbrot.playground/Contents.swift new file mode 100644 index 0000000..5e5fb45 --- /dev/null +++ b/Metalbrot.playground/Contents.swift @@ -0,0 +1,189 @@ +import XCPlayground +import Cocoa +import Metal +/*: + ## Drawing Fractals with Minimal Metal + + *[Jacob Bandes-Storch](http://bandes-stor.ch/), Feb 2016* + + This playground provides a small interactive example of how to use Metal to render visualizations of [fractals](https://en.wikipedia.org/wiki/Fractal) (namely, the [Mandelbrot set](https://en.wikipedia.org/wiki/Mandelbrot_set) and [Julia sets](https://en.wikipedia.org/wiki/Julia_set)). This certainly isn’t a comprehensive overview of Metal, but hopefully it’s easy to follow and modify. Enjoy! + + - Experiment: Click and drag on the fractal. Watch it change from the Mandelbrot set to a Julia set, and morph as you move the mouse. What happens if you click in a black area of the Mandelbrot set, as opposed to a colored area? + + - Note: To see the playground output, click the Assistant Editor button in the toolbar, or press ⌥⌘↩, which should display the Timeline. To see the extra functions in `Helpers.swift`, enable the Navigator via the toolbar or by pressing ⌘1. + ![navigation](navigation.png) + + - Note: This demo only covers using Metal for **compute functions**. There are a lot more complicated things you can do with the full rendering pipeline. Some further reading material is linked at the end of the playground. + */ +/*: + ---- + ### Setup + + To use Metal, we first need access to a connected graphics card (***device***). Since this is a small demo, we’ll prefer a low-power (integrated) graphics card. + + We’ll also need a ***command queue*** to let us send commands to the device, and a [dispatch queue](https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html) on which we’ll send these commands. + */ +let device = require(MTLCopyAllDevices().firstWhere{ $0.lowPower } ?? MTLCreateSystemDefaultDevice(), + orDie: "need a Metal device") + +let commandQueue = device.newCommandQueue() + +let drawingQueue = dispatch_queue_create("drawingQueue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0)) + +/*: + ---- + ***Shaders*** are small programs which run on the graphics card. + We can load the shader library from a separate file, `Shaders.metal` (which you can find in the left-hand Project navigator (⌘1) under **Resources**), and compile them on the fly for this device. This example uses two shaders, or ***compute kernels***, named `mandelbrotShader` and `juliaShader`. + */ +let shaderSource = require(try String(contentsOfURL: [#FileReference(fileReferenceLiteral: "Shaders.metal")#]), + orDie: "unable to read shader source file") + +let library = require(try device.newLibraryWithSource(shaderSource, options: nil), + orDie: "compiling shaders failed") + +//: - Experiment: Open up `Shaders.metal` and glance through it to understand what the shaders are doing. +//: - Important: If your shader has a syntax error, `newLibraryWithSource()` will throw an error here when it tries to compile the program. + +let mandelbrotShader = require(library.newFunctionWithName("mandelbrotShader"), + orDie: "unable to get mandelbrotShader") + +let juliaShader = require(library.newFunctionWithName("juliaShader"), + orDie: "unable to get juliaShader") + +//: The Julia set shader also needs some extra input, an *(x, y)* point, from the CPU. We can pass this via a shared buffer. +let juliaBuffer = device.newBufferWithLength(2 * sizeof(Float32), options: []) + +/*: + ---- + Before we can use these shaders, Metal needs to know how to request they be executed on the GPU. This information is precomputed and stored as ***compute pipeline state***, which we can reuse repeatedly. + + When executing the program, we’ll also have to decide how to utilize the GPU’s threads (how many groups of threads to use, and the number of threads per group). This will depend on the size of the view we want to draw into. + */ +let mandelbrotPipelineState = require(try device.newComputePipelineStateWithFunction(mandelbrotShader), + orDie: "unable to create compute pipeline state") + +let juliaPipelineState = require(try device.newComputePipelineStateWithFunction(juliaShader), + orDie: "unable to create compute pipeline state") + +var threadgroupSizes = ThreadgroupSizes.zeros // To be calculated later + +/*: + ---- + ### Drawing + + The fundamental way that Metal content gets onscreen is via CAMetalLayer. The layer has a pool of ***textures*** which hold image data. Our shaders will write into these textures, which can then be displayed on the screen. + + We’ll use a custom view class called `MetalView` which is backed by a CAMetalLayer, and automatically resizes its “drawable” (texture) to match the view’s size. (MetalKit provides the MTKView class, but it’s overkill for this demo.) + */ +let outputSize = CGSize(width: 300, height: 250) + +let metalView = MetalView(frame: CGRect(origin: .zero, size: outputSize), device: device) +let metalLayer = metalView.metalLayer +/*: + - Experiment: Look at the MetalView implementation to see how it interacts with the CAMetalLayer. + + A helper function called `computeAndDraw` in `Helpers.swift` takes care of encoding the commands which execute the shader, and submitting the buffer of encoded commands to the device. All we need to tell it is which pipeline state to use, which texture to draw into, and set up any necessary parameters to the shader functions. + */ +func drawMandelbrotSet() +{ + dispatch_async(drawingQueue) { + commandQueue.computeAndDraw(into: metalLayer.nextDrawable(), with: threadgroupSizes) { + $0.setComputePipelineState(mandelbrotPipelineState) + } + } +} + +func drawJuliaSet(point: CGPoint) +{ + dispatch_async(drawingQueue) { + commandQueue.computeAndDraw(into: metalLayer.nextDrawable(), with: threadgroupSizes) { + $0.setComputePipelineState(juliaPipelineState) + + // Pass the (x,y) coordinates of the clicked point via the buffer we allocated ahead of time. + $0.setBuffer(juliaBuffer, offset: 0, atIndex: 0) + let buf = UnsafeMutablePointer(juliaBuffer.contents()) + buf[0] = Float32(point.x) + buf[1] = Float32(point.y) + } + } +} +/*: + - Experiment: + Go check out the implementation of `computeAndDraw`! Can you understand how it works? + + ---- + ### The easy part + Now for some user interaction! Our view controller draws fractals when the view is first laid out, and whenever the mouse is dragged (user interaction requires Xcode 7.3). + */ +class Controller: NSViewController, MetalViewDelegate +{ + override func viewDidLayout() { + metalViewDrawableSizeDidChange(metalView) + } + func metalViewDrawableSizeDidChange(metalView: MetalView) { + // This helper function chooses how to assign the GPU’s threads to portions of the texture. + threadgroupSizes = mandelbrotPipelineState.threadgroupSizesForDrawableSize(metalView.metalLayer.drawableSize) + drawMandelbrotSet() + } + + override func mouseDown(event: NSEvent) { + drawJuliaSetForEvent(event) + } + override func mouseDragged(event: NSEvent) { + drawJuliaSetForEvent(event) + } + override func mouseUp(event: NSEvent) { + drawMandelbrotSet() + } + + func drawJuliaSetForEvent(event: NSEvent) { + var pos = metalView.convertPointToLayer(metalView.convertPoint(event.locationInWindow, fromView: nil)) + let scale = metalLayer.contentsScale + pos.x *= scale + pos.y *= scale + + drawJuliaSet(pos) + } +} + +//: Finally, we can put our view onscreen! +let controller = Controller() +controller.view = metalView +metalView.delegate = controller + +metalView.addSubview(Label(string: "Click me! (requires Xcode 7.3)"), at: CGPoint(x: 5, y: 5)) + +XCPlaygroundPage.currentPage.liveView = metalView + +/*: + ---- + ## What Next? + + I hope you’ve enjoyed (and learned something from) this demo. If you haven’t already, I encourage you to poke around in `Helpers.swift` and `Shaders.metal`. Try changing the code and see what happens — *take chances, make mistkaes, get messy!* + + If you like reading, there’s lots of reading material about Metal available from Apple, as well as excellent resources from others in the community. Just a few examples: + * Apple’s own [Metal for Developers](https://developer.apple.com/metal/) documentation and resources. + * [Metal By Example](http://metalbyexample.com/), a blog and book by Warren Moore. + * [Blog posts about Metal](http://redqueencoder.com/category/metal/) by The Red Queen Coder (Janie Clayton). + * [Posts and demos](http://flexmonkey.blogspot.co.uk/?view=magazine) by FlexMonkey (Simon Gladman) on topics including Metal and Core Image. + + ### Modify this playground! + + This demo barely scratches the surface. Here are a handful of ideas for things to try. (If you come up with something cool, I’d love to [hear about it](https://twitter.com/jtbandes)!) + + - Experiment: Tweak the `maxiters` and `escape` parameters in the shader source file. Do the fractals look different? Can you notice any difference in speed? Try modifying the playground to use a discrete graphics card, if your machine has one. + + - Experiment: Adapt this code to display the same fractals on an iOS device. (Metal isn’t supported in the iOS simulator.) You’ll need to use UIView instead of NSView, but most the Metal-related code can remain the same. + + - Experiment: Choose another fractal or another coloring scheme, and modify `Shaders.metal` to render it. + + - Experiment: Add a label which shows the coordinates that were clicked in the complex plane. + + Bonus: can you share the code which does this x,y-to-complex conversion between Swift and the shader itself? Try moving things into a full Xcode project and setting up a [bridging header](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html). You might want to use `#ifdef __cplusplus` and/or `extern "C"`. + + - Experiment: Try using [MTKView](https://developer.apple.com/library/ios/documentation/MetalKit/Reference/MTKView_ClassReference/) instead of the simple `MetalView` in this playground. Use the MTKView’s delegate or a subclass to render each frame, and modify the pipeline so that your fractals can change over time. + * Try to make the colors change slowly over time. + * Try to make the visualization zoom in on an [interesting point](https://en.wikipedia.org/wiki/Mandelbrot_set#Image_gallery_of_a_zoom_sequence). + + - Experiment: Use `CIImage.init(MTLTexture:options:)` to render the fractals into an image. Save an animated GIF using [CGImageDestination](http://stackoverflow.com/q/14915138/23649), or a movie using [AVAssetWriter](http://stackoverflow.com/q/3741323/23649). + */ diff --git a/Metalbrot.playground/Resources/Shaders.metal b/Metalbrot.playground/Resources/Shaders.metal new file mode 100644 index 0000000..ef2d9ff --- /dev/null +++ b/Metalbrot.playground/Resources/Shaders.metal @@ -0,0 +1,110 @@ +#include +using namespace metal; + +#define M_PI 3.141592653589793238462643383 + +/// Basic implementation of complex numbers, with * + - operators, and a function to return the squared magnitude. +template +struct complex +{ + T _x, _y; + + complex(T x, T y) : _x(x), _y(y) { } + + T sqmag() const { + return _x*_x + _y*_y; + } + + complex operator*(const thread complex& other) const { + return complex(_x*other._x - _y*other._y, + _x*other._y + _y*other._x); + } + + complex operator+(const thread complex& other) const { + return complex(_x + other._x, _y + other._y); + } + + complex operator-(const thread complex& other) const { + return complex(_x - other._x, _y - other._y); + } + + complex operator*(const thread T& c) const { + return complex(_x * c, _y * c); + } +}; + + +#define complexExtentX 3.5 +#define complexExtentY 3 +#define mandelbrotShiftX 0.2 + +/// Convert a point on the screen to a point in the complex plane. +template +complex screenToComplex(T x, T y, T width, T height) +{ + const T scale = max(complexExtentX/width, complexExtentY/height); + + return complex((x-width/2)*scale, (y-height/2)*scale); +} + + +/// Both Mandelbrot and Julia sets use the same iterative algorithm to determine whether +/// a given point is in the set. Each point is colored based on how quickly it escape to infinity. +template +float4 colorForIteration(complex z, complex c, int maxiters, float escape) +{ + for (int i = 0; i < maxiters; i++) { + z = z*z + c; + if (z.sqmag() > escape) { + // Smoothing coloring, adapted from: + // + float hue = (i+1-log2(log10(z.sqmag())/2))/maxiters*4 * M_PI + 3; + + // Convert to RGB + return float4((cos(hue)+1)/2, + (-cos(hue+M_PI/3)+1)/2, + (-cos(hue-M_PI/3)+1)/2, + 1); + } + } + + return float4(0, 0, 0, 1); +} + + +/// Render a visualization of the Mandelbrot set into the `output` texture. +kernel void mandelbrotShader(texture2d output [[texture(0)]], + uint2 upos [[thread_position_in_grid]]) +{ + int width = output.get_width(); + int height = output.get_height(); + if (upos.x > width || upos.y > height) return; + + complex z(0, 0); + + complex c = screenToComplex(upos.x - mandelbrotShiftX*width, + upos.y, + width, height); + + output.write(float4(colorForIteration(z, c, 100, 100)), upos); +} + + +/// Render a visualization of the Julia set for the point `screenPoint` into the `output` texture. +kernel void juliaShader(texture2d output [[texture(0)]], + uint2 upos [[thread_position_in_grid]], + const device float2& screenPoint [[buffer(0)]]) +{ + int width = output.get_width(); + int height = output.get_height(); + if (upos.x > width || upos.y > height) return; + + complex z = screenToComplex(upos.x, upos.y, width, height); + + complex c = screenToComplex(screenPoint.x - mandelbrotShiftX*width, + screenPoint.y, + width, + height); + + output.write(float4(colorForIteration(z, c, 100, 50)), upos); +} diff --git a/Metalbrot.playground/Resources/navigation.png b/Metalbrot.playground/Resources/navigation.png new file mode 100644 index 0000000..32b7e04 Binary files /dev/null and b/Metalbrot.playground/Resources/navigation.png differ diff --git a/Metalbrot.playground/Sources/Helpers.swift b/Metalbrot.playground/Sources/Helpers.swift new file mode 100644 index 0000000..bcfae5c --- /dev/null +++ b/Metalbrot.playground/Sources/Helpers.swift @@ -0,0 +1,224 @@ +import XCPlayground +import Cocoa +import Metal + +/// A view which is backed by a CAMetalLayer, automatically updating its drawableSize and contentsScale as appropriate. +public class MetalView: NSView +{ + @available(*, unavailable) public required init?(coder: NSCoder) { fatalError() } + + public let metalLayer = CAMetalLayer() + public weak var delegate: MetalViewDelegate? + + public init(frame: CGRect, device: MTLDevice) { + super.init(frame: frame) + metalLayer.device = device + wantsLayer = true + updateDrawableSize(contentsScale: metalLayer.contentsScale) + } + + public override func makeBackingLayer() -> CALayer { + return metalLayer + } + + public override func layer(layer: CALayer, shouldInheritContentsScale newScale: CGFloat, fromWindow window: NSWindow) -> Bool { + updateDrawableSize(contentsScale: newScale) + return true + } + + private func updateDrawableSize(contentsScale scale: CGFloat) { + var size = metalLayer.bounds.size + size.width *= scale + size.height *= scale + metalLayer.drawableSize = size + delegate?.metalViewDrawableSizeDidChange(self) + } +} + + +public protocol MetalViewDelegate: class +{ + func metalViewDrawableSizeDidChange(metalView: MetalView) +} + + +extension MTLSize +{ + var hasZeroDimension: Bool { + return depth == 0 || width == 0 || height == 0 + } +} + + +/// Encapsulates the sizes to be passed to `MTLComputeCommandEncoder.dispatchThreadgroups(_:threadsPerThreadgroup:)`. +public struct ThreadgroupSizes +{ + var threadsPerThreadgroup: MTLSize + var threadgroupsPerGrid: MTLSize + + public static let zeros = ThreadgroupSizes( + threadsPerThreadgroup: MTLSize(), + threadgroupsPerGrid: MTLSize()) + + var hasZeroDimension: Bool { + return threadsPerThreadgroup.hasZeroDimension || threadgroupsPerGrid.hasZeroDimension + } +} + + + +public extension MTLCommandQueue +{ + /// Helper function for running compute kernels and displaying the output onscreen. + /// + /// This function configures a MTLComputeCommandEncoder by setting the given `drawable`'s texture + /// as the 0th texture (so it will be available as a `[[texture(0)]]` parameter in the kernel). + /// It calls `drawBlock` to allow further configuration, then dispatches the threadgroups and + /// presents the results. + /// + /// - Requires: `drawBlock` must call `setComputePipelineState` on the command encoder to select a compute function. + func computeAndDraw(@autoclosure into drawable: () -> CAMetalDrawable?, with threadgroupSizes: ThreadgroupSizes, @noescape drawBlock: MTLComputeCommandEncoder -> Void) + { + if threadgroupSizes.hasZeroDimension { + print("dimensions are zero; not drawing") + return + } + + autoreleasepool { // Ensure drawables are freed for the system to allocate new ones. + guard let drawable = drawable() else { + print("no drawable") + return + } + + let buffer = self.commandBuffer() + let encoder = buffer.computeCommandEncoder() + encoder.setTexture(drawable.texture, atIndex: 0) + + drawBlock(encoder) + + encoder.dispatchThreadgroups(threadgroupSizes.threadgroupsPerGrid, threadsPerThreadgroup: threadgroupSizes.threadsPerThreadgroup) + encoder.endEncoding() + + buffer.presentDrawable(drawable) + buffer.commit() + buffer.waitUntilCompleted() + } + } +} + + +public extension MTLComputePipelineState +{ + /// Selects "reasonable" values for threadsPerThreadgroup and threadgroupsPerGrid for the given `drawableSize`. + /// - Remark: The heuristics used here are not perfect. There are many ways to underutilize the GPU, + /// including selecting suboptimal threadgroup sizes, or branching in the shader code. + /// + /// If you are certain you can always use threadgroups with a multiple of `threadExecutionWidth` + /// threads, then you may want to use MTLComputePipleineDescriptor and its property + /// `threadGroupSizeIsMultipleOfThreadExecutionWidth` to configure your pipeline state. + /// + /// If your shader is doing some more interesting calculations, and your threads need to share memory in some + /// meaningful way, then you’ll probably want to do something less generalized to choose your threadgroups. + func threadgroupSizesForDrawableSize(drawableSize: CGSize) -> ThreadgroupSizes + { + let waveSize = self.threadExecutionWidth + let maxThreadsPerGroup = self.maxTotalThreadsPerThreadgroup + + let drawableWidth = Int(drawableSize.width) + let drawableHeight = Int(drawableSize.height) + + if drawableWidth == 0 || drawableHeight == 0 { + print("drawableSize is zero") + return .zeros + } + + // Determine the set of possible sizes (not exceeding maxThreadsPerGroup). + var candidates: [ThreadgroupSizes] = [] + for groupWidth in 1...maxThreadsPerGroup { + for groupHeight in 1...(maxThreadsPerGroup/groupWidth) { + // Round up the number of groups to ensure the entire drawable size is covered. + // + let groupsPerGrid = MTLSize(width: (drawableWidth + groupWidth - 1) / groupWidth, + height: (drawableHeight + groupHeight - 1) / groupHeight, + depth: 1) + + candidates.append(ThreadgroupSizes( + threadsPerThreadgroup: MTLSize(width: groupWidth, height: groupHeight, depth: 1), + threadgroupsPerGrid: groupsPerGrid)) + } + } + + /// Make a rough approximation for how much compute power will be "wasted" (e.g. when the total number + /// of threads in a group isn’t an even multiple of `threadExecutionWidth`, or when the total number of + /// threads being dispatched exceeds the drawable size). Smaller is better. + func _estimatedUnderutilization(s: ThreadgroupSizes) -> Int { + let excessWidth = s.threadsPerThreadgroup.width * s.threadgroupsPerGrid.width - drawableWidth + let excessHeight = s.threadsPerThreadgroup.height * s.threadgroupsPerGrid.height - drawableHeight + + let totalThreadsPerGroup = s.threadsPerThreadgroup.width * s.threadsPerThreadgroup.height + let totalGroups = s.threadgroupsPerGrid.width * s.threadgroupsPerGrid.height + + let excessArea = excessWidth * drawableHeight + excessHeight * drawableWidth + excessWidth * excessHeight + let excessThreadsPerGroup = (waveSize - totalThreadsPerGroup % waveSize) % waveSize + + return excessArea + excessThreadsPerGroup * totalGroups + } + + // Choose the threadgroup sizes which waste the least amount of execution time/power. + let result = candidates.minElement { _estimatedUnderutilization($0) < _estimatedUnderutilization($1) } + return result ?? .zeros + } +} + + +/// Playground helper: produce the value of `expr`, or print the given `message` and exit. +/// If `expr` throws an error, the error is also printed. +public func require(@autoclosure expr: () throws -> T?, @autoclosure orDie message: () -> String) -> T +{ + do { + if let result = try expr() { return result } + else { print(message()) } + } + catch { + print(message()) + print("error: \(error)") + } + XCPlaygroundPage.currentPage.finishExecution() +} + + +public extension SequenceType +{ + /// - Returns: The first element in `self` which matches the given `predicate`. + func firstWhere(@noescape predicate: Generator.Element throws -> Bool) rethrows -> Generator.Element? + { + return try lazy.filter(predicate).first + } +} + + +public extension NSView +{ + func addSubview(subview: NSView, at origin: CGPoint) + { + addSubview(subview) + subview.frame.origin = origin + } +} + + +public class Label: NSTextField +{ + @available(*, unavailable) public required init?(coder: NSCoder) { fatalError() } + + public init(string: String) { + super.init(frame: .zero) + selectable = false + editable = false + bordered = false + drawsBackground = false + textColor = .whiteColor() + stringValue = string + sizeToFit() + } +} diff --git a/Metalbrot.playground/contents.xcplayground b/Metalbrot.playground/contents.xcplayground new file mode 100644 index 0000000..50ce46d --- /dev/null +++ b/Metalbrot.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Metalbrot.playground/playground.xcworkspace/contents.xcworkspacedata b/Metalbrot.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Metalbrot.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Metalbrot.playground/timeline.xctimeline b/Metalbrot.playground/timeline.xctimeline new file mode 100644 index 0000000..f46c0b4 --- /dev/null +++ b/Metalbrot.playground/timeline.xctimeline @@ -0,0 +1,11 @@ + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..7beafc9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +`Metalbrot.playground` is an [interactive playground](https://developer.apple.com/swift/blog/?id=35) showing how to use Metal compute kernels with Swift. More information can be found [on my blog](http://bandes-stor.ch/blog/2016/02/21/drawing-fractals-with-minimal-metal/). + +(This demo is free for personal and educational use. If you plan to use it for anything else, please credit me — see LICENSE.txt for details.)